parent
471a525b59
commit
f98cd0a5c0
@ -0,0 +1,51 @@ |
||||
/*Package log 是kratos日志库. |
||||
|
||||
一、主要功能: |
||||
|
||||
1. 日志打印到本地 |
||||
2. 日志打印到标准输出 |
||||
3. verbose日志实现,参考glog实现,可通过设置不同verbose级别,默认不开启 |
||||
|
||||
二、日志配置 |
||||
|
||||
1. 默认配置 |
||||
|
||||
目前日志已经实现默认配置。可以直接使用以下方式: |
||||
log.Init(nil) |
||||
|
||||
2. 启动参数 or 环境变量 |
||||
|
||||
启动参数 环境变量 说明 |
||||
log.stdout LOG_STDOUT 是否开启标准输出 |
||||
log.dir LOG_DIR 文件日志路径 |
||||
log.v LOG_V verbose日志级别 |
||||
log.module LOG_MODULE 可单独配置每个文件的verbose级别:file=1,file2=2 |
||||
log.filter LOG_FILTER 配置需要过滤的字段:field1,field2 |
||||
|
||||
3. 配置文件 |
||||
但是如果有特殊需要可以走一下格式配置: |
||||
[log] |
||||
family = "xxx-service" |
||||
dir = "/data/log/xxx-service/" |
||||
stdout = true |
||||
vLevel = 3 |
||||
filter = ["fileld1", "field2"] |
||||
[log.module] |
||||
"dao_user" = 2 |
||||
"servic*" = 1 |
||||
|
||||
三、配置说明 |
||||
|
||||
1.log |
||||
|
||||
family 项目名,默认读环境变量$APPID |
||||
studout 标准输出,prod环境不建议开启 |
||||
filter 配置需要过滤掉的字段,以“***”替换 |
||||
dir 文件日志地址,prod环境不建议开启 |
||||
v 开启verbose级别日志,可指定全局级别 |
||||
|
||||
2. log.module |
||||
|
||||
可单独配置每个文件的verbose级别 |
||||
*/ |
||||
package log |
@ -0,0 +1,92 @@ |
||||
package log |
||||
|
||||
import ( |
||||
"context" |
||||
"io" |
||||
"path/filepath" |
||||
"time" |
||||
|
||||
"github.com/bilibili/Kratos/pkg/log/internal/filewriter" |
||||
) |
||||
|
||||
// level idx
|
||||
const ( |
||||
_infoIdx = iota |
||||
_warnIdx |
||||
_errorIdx |
||||
_totalIdx |
||||
) |
||||
|
||||
var _fileNames = map[int]string{ |
||||
_infoIdx: "info.log", |
||||
_warnIdx: "warning.log", |
||||
_errorIdx: "error.log", |
||||
} |
||||
|
||||
// FileHandler .
|
||||
type FileHandler struct { |
||||
render Render |
||||
fws [_totalIdx]*filewriter.FileWriter |
||||
} |
||||
|
||||
// NewFile crete a file logger.
|
||||
func NewFile(dir string, bufferSize, rotateSize int64, maxLogFile int) *FileHandler { |
||||
// new info writer
|
||||
newWriter := func(name string) *filewriter.FileWriter { |
||||
var options []filewriter.Option |
||||
if rotateSize > 0 { |
||||
options = append(options, filewriter.MaxSize(rotateSize)) |
||||
} |
||||
if maxLogFile > 0 { |
||||
options = append(options, filewriter.MaxFile(maxLogFile)) |
||||
} |
||||
w, err := filewriter.New(filepath.Join(dir, name), options...) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return w |
||||
} |
||||
handler := &FileHandler{ |
||||
render: newPatternRender("[%D %T] [%L] [%S] %M"), |
||||
} |
||||
for idx, name := range _fileNames { |
||||
handler.fws[idx] = newWriter(name) |
||||
} |
||||
return handler |
||||
} |
||||
|
||||
// Log loggint to file .
|
||||
func (h *FileHandler) Log(ctx context.Context, lv Level, args ...D) { |
||||
d := make(map[string]interface{}, 10+len(args)) |
||||
for _, arg := range args { |
||||
d[arg.Key] = arg.Value |
||||
} |
||||
// add extra fields
|
||||
addExtraField(ctx, d) |
||||
d[_time] = time.Now().Format(_timeFormat) |
||||
var w io.Writer |
||||
switch lv { |
||||
case _warnLevel: |
||||
w = h.fws[_warnIdx] |
||||
case _errorLevel: |
||||
w = h.fws[_errorIdx] |
||||
default: |
||||
w = h.fws[_infoIdx] |
||||
} |
||||
h.render.Render(w, d) |
||||
w.Write([]byte("\n")) |
||||
} |
||||
|
||||
// Close log handler
|
||||
func (h *FileHandler) Close() error { |
||||
for _, fw := range h.fws { |
||||
// ignored error
|
||||
fw.Close() |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// SetFormat set log format
|
||||
func (h *FileHandler) SetFormat(format string) { |
||||
h.render = newPatternRender(format) |
||||
} |
@ -0,0 +1,21 @@ |
||||
COPY FROM: https://github.com/uber-go/zap |
||||
|
||||
Copyright (c) 2016-2017 Uber Technologies, Inc. |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in |
||||
all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
THE SOFTWARE. |
@ -0,0 +1,97 @@ |
||||
package core |
||||
|
||||
import "strconv" |
||||
|
||||
const _size = 1024 // by default, create 1 KiB buffers
|
||||
|
||||
// NewBuffer is new buffer
|
||||
func NewBuffer(_size int) *Buffer { |
||||
return &Buffer{bs: make([]byte, 0, _size)} |
||||
} |
||||
|
||||
// Buffer is a thin wrapper around a byte slice. It's intended to be pooled, so
|
||||
// the only way to construct one is via a Pool.
|
||||
type Buffer struct { |
||||
bs []byte |
||||
pool Pool |
||||
} |
||||
|
||||
// AppendByte writes a single byte to the Buffer.
|
||||
func (b *Buffer) AppendByte(v byte) { |
||||
b.bs = append(b.bs, v) |
||||
} |
||||
|
||||
// AppendString writes a string to the Buffer.
|
||||
func (b *Buffer) AppendString(s string) { |
||||
b.bs = append(b.bs, s...) |
||||
} |
||||
|
||||
// AppendInt appends an integer to the underlying buffer (assuming base 10).
|
||||
func (b *Buffer) AppendInt(i int64) { |
||||
b.bs = strconv.AppendInt(b.bs, i, 10) |
||||
} |
||||
|
||||
// AppendUint appends an unsigned integer to the underlying buffer (assuming
|
||||
// base 10).
|
||||
func (b *Buffer) AppendUint(i uint64) { |
||||
b.bs = strconv.AppendUint(b.bs, i, 10) |
||||
} |
||||
|
||||
// AppendBool appends a bool to the underlying buffer.
|
||||
func (b *Buffer) AppendBool(v bool) { |
||||
b.bs = strconv.AppendBool(b.bs, v) |
||||
} |
||||
|
||||
// AppendFloat appends a float to the underlying buffer. It doesn't quote NaN
|
||||
// or +/- Inf.
|
||||
func (b *Buffer) AppendFloat(f float64, bitSize int) { |
||||
b.bs = strconv.AppendFloat(b.bs, f, 'f', -1, bitSize) |
||||
} |
||||
|
||||
// Len returns the length of the underlying byte slice.
|
||||
func (b *Buffer) Len() int { |
||||
return len(b.bs) |
||||
} |
||||
|
||||
// Cap returns the capacity of the underlying byte slice.
|
||||
func (b *Buffer) Cap() int { |
||||
return cap(b.bs) |
||||
} |
||||
|
||||
// Bytes returns a mutable reference to the underlying byte slice.
|
||||
func (b *Buffer) Bytes() []byte { |
||||
return b.bs |
||||
} |
||||
|
||||
// String returns a string copy of the underlying byte slice.
|
||||
func (b *Buffer) String() string { |
||||
return string(b.bs) |
||||
} |
||||
|
||||
// Reset resets the underlying byte slice. Subsequent writes re-use the slice's
|
||||
// backing array.
|
||||
func (b *Buffer) Reset() { |
||||
b.bs = b.bs[:0] |
||||
} |
||||
|
||||
// Write implements io.Writer.
|
||||
func (b *Buffer) Write(bs []byte) (int, error) { |
||||
b.bs = append(b.bs, bs...) |
||||
return len(bs), nil |
||||
} |
||||
|
||||
// TrimNewline trims any final "\n" byte from the end of the buffer.
|
||||
func (b *Buffer) TrimNewline() { |
||||
if i := len(b.bs) - 1; i >= 0 { |
||||
if b.bs[i] == '\n' { |
||||
b.bs = b.bs[:i] |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Free returns the Buffer to its Pool.
|
||||
//
|
||||
// Callers must not retain references to the Buffer after calling Free.
|
||||
func (b *Buffer) Free() { |
||||
b.pool.put(b) |
||||
} |
@ -0,0 +1,71 @@ |
||||
package core |
||||
|
||||
import ( |
||||
"bytes" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestBufferWrites(t *testing.T) { |
||||
buf := NewPool(0).Get() |
||||
|
||||
tests := []struct { |
||||
desc string |
||||
f func() |
||||
want string |
||||
}{ |
||||
{"AppendByte", func() { buf.AppendByte('v') }, "v"}, |
||||
{"AppendString", func() { buf.AppendString("foo") }, "foo"}, |
||||
{"AppendIntPositive", func() { buf.AppendInt(42) }, "42"}, |
||||
{"AppendIntNegative", func() { buf.AppendInt(-42) }, "-42"}, |
||||
{"AppendUint", func() { buf.AppendUint(42) }, "42"}, |
||||
{"AppendBool", func() { buf.AppendBool(true) }, "true"}, |
||||
{"AppendFloat64", func() { buf.AppendFloat(3.14, 64) }, "3.14"}, |
||||
// Intenationally introduce some floating-point error.
|
||||
{"AppendFloat32", func() { buf.AppendFloat(float64(float32(3.14)), 32) }, "3.14"}, |
||||
{"AppendWrite", func() { buf.Write([]byte("foo")) }, "foo"}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.desc, func(t *testing.T) { |
||||
buf.Reset() |
||||
tt.f() |
||||
assert.Equal(t, tt.want, buf.String(), "Unexpected buffer.String().") |
||||
assert.Equal(t, tt.want, string(buf.Bytes()), "Unexpected string(buffer.Bytes()).") |
||||
assert.Equal(t, len(tt.want), buf.Len(), "Unexpected buffer length.") |
||||
// We're not writing more than a kibibyte in tests.
|
||||
assert.Equal(t, _size, buf.Cap(), "Expected buffer capacity to remain constant.") |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func BenchmarkBuffers(b *testing.B) { |
||||
// Because we use the strconv.AppendFoo functions so liberally, we can't
|
||||
// use the standard library's bytes.Buffer anyways (without incurring a
|
||||
// bunch of extra allocations). Nevertheless, let's make sure that we're
|
||||
// not losing any precious nanoseconds.
|
||||
str := strings.Repeat("a", 1024) |
||||
slice := make([]byte, 1024) |
||||
buf := bytes.NewBuffer(slice) |
||||
custom := NewPool(0).Get() |
||||
b.Run("ByteSlice", func(b *testing.B) { |
||||
for i := 0; i < b.N; i++ { |
||||
slice = append(slice, str...) |
||||
slice = slice[:0] |
||||
} |
||||
}) |
||||
b.Run("BytesBuffer", func(b *testing.B) { |
||||
for i := 0; i < b.N; i++ { |
||||
buf.WriteString(str) |
||||
buf.Reset() |
||||
} |
||||
}) |
||||
b.Run("CustomBuffer", func(b *testing.B) { |
||||
for i := 0; i < b.N; i++ { |
||||
custom.AppendString(str) |
||||
custom.Reset() |
||||
} |
||||
}) |
||||
} |
@ -0,0 +1,7 @@ |
||||
package core |
||||
|
||||
var ( |
||||
_pool = NewPool(_size) |
||||
// GetPool retrieves a buffer from the pool, creating one if necessary.
|
||||
GetPool = _pool.Get |
||||
) |
@ -0,0 +1,187 @@ |
||||
package core |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
// DefaultLineEnding defines the default line ending when writing logs.
|
||||
// Alternate line endings specified in EncoderConfig can override this
|
||||
// behavior.
|
||||
const DefaultLineEnding = "\n" |
||||
|
||||
// ObjectEncoder is a strongly-typed, encoding-agnostic interface for adding a
|
||||
// map- or struct-like object to the logging context. Like maps, ObjectEncoders
|
||||
// aren't safe for concurrent use (though typical use shouldn't require locks).
|
||||
type ObjectEncoder interface { |
||||
// Logging-specific marshalers.
|
||||
AddArray(key string, marshaler ArrayMarshaler) error |
||||
AddObject(key string, marshaler ObjectMarshaler) error |
||||
|
||||
// Built-in types.
|
||||
AddBinary(key string, value []byte) // for arbitrary bytes
|
||||
AddByteString(key string, value []byte) // for UTF-8 encoded bytes
|
||||
AddBool(key string, value bool) |
||||
AddComplex128(key string, value complex128) |
||||
AddComplex64(key string, value complex64) |
||||
AddDuration(key string, value time.Duration) |
||||
AddFloat64(key string, value float64) |
||||
AddFloat32(key string, value float32) |
||||
AddInt(key string, value int) |
||||
AddInt64(key string, value int64) |
||||
AddInt32(key string, value int32) |
||||
AddInt16(key string, value int16) |
||||
AddInt8(key string, value int8) |
||||
AddString(key, value string) |
||||
AddTime(key string, value time.Time) |
||||
AddUint(key string, value uint) |
||||
AddUint64(key string, value uint64) |
||||
AddUint32(key string, value uint32) |
||||
AddUint16(key string, value uint16) |
||||
AddUint8(key string, value uint8) |
||||
AddUintptr(key string, value uintptr) |
||||
|
||||
// AddReflected uses reflection to serialize arbitrary objects, so it's slow
|
||||
// and allocation-heavy.
|
||||
AddReflected(key string, value interface{}) error |
||||
// OpenNamespace opens an isolated namespace where all subsequent fields will
|
||||
// be added. Applications can use namespaces to prevent key collisions when
|
||||
// injecting loggers into sub-components or third-party libraries.
|
||||
OpenNamespace(key string) |
||||
} |
||||
|
||||
// ObjectMarshaler allows user-defined types to efficiently add themselves to the
|
||||
// logging context, and to selectively omit information which shouldn't be
|
||||
// included in logs (e.g., passwords).
|
||||
type ObjectMarshaler interface { |
||||
MarshalLogObject(ObjectEncoder) error |
||||
} |
||||
|
||||
// ObjectMarshalerFunc is a type adapter that turns a function into an
|
||||
// ObjectMarshaler.
|
||||
type ObjectMarshalerFunc func(ObjectEncoder) error |
||||
|
||||
// MarshalLogObject calls the underlying function.
|
||||
func (f ObjectMarshalerFunc) MarshalLogObject(enc ObjectEncoder) error { |
||||
return f(enc) |
||||
} |
||||
|
||||
// ArrayMarshaler allows user-defined types to efficiently add themselves to the
|
||||
// logging context, and to selectively omit information which shouldn't be
|
||||
// included in logs (e.g., passwords).
|
||||
type ArrayMarshaler interface { |
||||
MarshalLogArray(ArrayEncoder) error |
||||
} |
||||
|
||||
// ArrayMarshalerFunc is a type adapter that turns a function into an
|
||||
// ArrayMarshaler.
|
||||
type ArrayMarshalerFunc func(ArrayEncoder) error |
||||
|
||||
// MarshalLogArray calls the underlying function.
|
||||
func (f ArrayMarshalerFunc) MarshalLogArray(enc ArrayEncoder) error { |
||||
return f(enc) |
||||
} |
||||
|
||||
// ArrayEncoder is a strongly-typed, encoding-agnostic interface for adding
|
||||
// array-like objects to the logging context. Of note, it supports mixed-type
|
||||
// arrays even though they aren't typical in Go. Like slices, ArrayEncoders
|
||||
// aren't safe for concurrent use (though typical use shouldn't require locks).
|
||||
type ArrayEncoder interface { |
||||
// Built-in types.
|
||||
PrimitiveArrayEncoder |
||||
|
||||
// Time-related types.
|
||||
AppendDuration(time.Duration) |
||||
AppendTime(time.Time) |
||||
|
||||
// Logging-specific marshalers.
|
||||
AppendArray(ArrayMarshaler) error |
||||
AppendObject(ObjectMarshaler) error |
||||
|
||||
// AppendReflected uses reflection to serialize arbitrary objects, so it's
|
||||
// slow and allocation-heavy.
|
||||
AppendReflected(value interface{}) error |
||||
} |
||||
|
||||
// PrimitiveArrayEncoder is the subset of the ArrayEncoder interface that deals
|
||||
// only in Go's built-in types. It's included only so that Duration- and
|
||||
// TimeEncoders cannot trigger infinite recursion.
|
||||
type PrimitiveArrayEncoder interface { |
||||
// Built-in types.
|
||||
AppendBool(bool) |
||||
AppendByteString([]byte) // for UTF-8 encoded bytes
|
||||
AppendComplex128(complex128) |
||||
AppendComplex64(complex64) |
||||
AppendFloat64(float64) |
||||
AppendFloat32(float32) |
||||
AppendInt(int) |
||||
AppendInt64(int64) |
||||
AppendInt32(int32) |
||||
AppendInt16(int16) |
||||
AppendInt8(int8) |
||||
AppendString(string) |
||||
AppendUint(uint) |
||||
AppendUint64(uint64) |
||||
AppendUint32(uint32) |
||||
AppendUint16(uint16) |
||||
AppendUint8(uint8) |
||||
AppendUintptr(uintptr) |
||||
} |
||||
|
||||
// An EncoderConfig allows users to configure the concrete encoders supplied by
|
||||
// zapcore.
|
||||
type EncoderConfig struct { |
||||
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"` |
||||
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"` |
||||
// Configure the primitive representations of common complex types. For
|
||||
// example, some users may want all time.Times serialized as floating-point
|
||||
// seconds since epoch, while others may prefer ISO8601 strings.
|
||||
/*EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"` |
||||
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"` |
||||
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"` |
||||
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"` |
||||
// Unlike the other primitive type encoders, EncodeName is optional. The
|
||||
// zero value falls back to FullNameEncoder.
|
||||
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`*/ |
||||
} |
||||
|
||||
// Encoder is a format-agnostic interface for all log entry marshalers. Since
|
||||
// log encoders don't need to support the same wide range of use cases as
|
||||
// general-purpose marshalers, it's possible to make them faster and
|
||||
// lower-allocation.
|
||||
//
|
||||
// Implementations of the ObjectEncoder interface's methods can, of course,
|
||||
// freely modify the receiver. However, the Clone and EncodeEntry methods will
|
||||
// be called concurrently and shouldn't modify the receiver.
|
||||
type Encoder interface { |
||||
ObjectEncoder |
||||
|
||||
// Clone copies the encoder, ensuring that adding fields to the copy doesn't
|
||||
// affect the original.
|
||||
Clone() Encoder |
||||
|
||||
// EncodeEntry encodes an entry and fields, along with any accumulated
|
||||
// context, into a byte buffer and returns it.
|
||||
Encode(*Buffer, ...Field) error |
||||
} |
||||
|
||||
// A TimeEncoder serializes a time.Time to a primitive type.
|
||||
type TimeEncoder func(time.Time, PrimitiveArrayEncoder) |
||||
|
||||
// A DurationEncoder serializes a time.Duration to a primitive type.
|
||||
type DurationEncoder func(time.Duration, PrimitiveArrayEncoder) |
||||
|
||||
// EpochTimeEncoder serializes a time.Time to a floating-point number of seconds
|
||||
// since the Unix epoch.
|
||||
func EpochTimeEncoder(t time.Time, enc PrimitiveArrayEncoder) { |
||||
//var d []byte
|
||||
enc.AppendString(t.Format("2006-01-02T15:04:05.999999")) |
||||
//enc.AppendByteString(t.AppendFormat(d, "2006-01-02T15:04:05.999999"))
|
||||
/*nanos := t.UnixNano() |
||||
sec := float64(nanos) / float64(time.Second) |
||||
enc.AppendFloat64(sec)*/ |
||||
} |
||||
|
||||
// SecondsDurationEncoder serializes a time.Duration to a floating-point number of seconds elapsed.
|
||||
func SecondsDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder) { |
||||
enc.AppendFloat64(float64(d) / float64(time.Second)) |
||||
} |
@ -0,0 +1,6 @@ |
||||
package core |
||||
|
||||
// Field is for encoder
|
||||
type Field interface { |
||||
AddTo(enc ObjectEncoder) |
||||
} |
@ -0,0 +1,344 @@ |
||||
package filewriter |
||||
|
||||
import ( |
||||
"bytes" |
||||
"container/list" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"log" |
||||
"os" |
||||
"path/filepath" |
||||
"sort" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"sync/atomic" |
||||
"time" |
||||
) |
||||
|
||||
// FileWriter create file log writer
|
||||
type FileWriter struct { |
||||
opt option |
||||
dir string |
||||
fname string |
||||
ch chan *bytes.Buffer |
||||
stdlog *log.Logger |
||||
pool *sync.Pool |
||||
|
||||
lastRotateFormat string |
||||
lastSplitNum int |
||||
|
||||
current *wrapFile |
||||
files *list.List |
||||
|
||||
closed int32 |
||||
wg sync.WaitGroup |
||||
} |
||||
|
||||
type rotateItem struct { |
||||
rotateTime int64 |
||||
rotateNum int |
||||
fname string |
||||
} |
||||
|
||||
func parseRotateItem(dir, fname, rotateFormat string) (*list.List, error) { |
||||
fis, err := ioutil.ReadDir(dir) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// parse exists log file filename
|
||||
parse := func(s string) (rt rotateItem, err error) { |
||||
// remove filename and left "." error.log.2018-09-12.001 -> 2018-09-12.001
|
||||
rt.fname = s |
||||
s = strings.TrimLeft(s[len(fname):], ".") |
||||
seqs := strings.Split(s, ".") |
||||
var t time.Time |
||||
switch len(seqs) { |
||||
case 2: |
||||
if rt.rotateNum, err = strconv.Atoi(seqs[1]); err != nil { |
||||
return |
||||
} |
||||
fallthrough |
||||
case 1: |
||||
if t, err = time.Parse(rotateFormat, seqs[0]); err != nil { |
||||
return |
||||
} |
||||
rt.rotateTime = t.Unix() |
||||
} |
||||
return |
||||
} |
||||
|
||||
var items []rotateItem |
||||
for _, fi := range fis { |
||||
if strings.HasPrefix(fi.Name(), fname) && fi.Name() != fname { |
||||
rt, err := parse(fi.Name()) |
||||
if err != nil { |
||||
// TODO deal with error
|
||||
continue |
||||
} |
||||
items = append(items, rt) |
||||
} |
||||
} |
||||
sort.Slice(items, func(i, j int) bool { |
||||
if items[i].rotateTime == items[j].rotateTime { |
||||
return items[i].rotateNum > items[j].rotateNum |
||||
} |
||||
return items[i].rotateTime > items[j].rotateTime |
||||
}) |
||||
l := list.New() |
||||
|
||||
for _, item := range items { |
||||
l.PushBack(item) |
||||
} |
||||
return l, nil |
||||
} |
||||
|
||||
type wrapFile struct { |
||||
fsize int64 |
||||
fp *os.File |
||||
} |
||||
|
||||
func (w *wrapFile) size() int64 { |
||||
return w.fsize |
||||
} |
||||
|
||||
func (w *wrapFile) write(p []byte) (n int, err error) { |
||||
n, err = w.fp.Write(p) |
||||
w.fsize += int64(n) |
||||
return |
||||
} |
||||
|
||||
func newWrapFile(fpath string) (*wrapFile, error) { |
||||
fp, err := os.OpenFile(fpath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
fi, err := fp.Stat() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &wrapFile{fp: fp, fsize: fi.Size()}, nil |
||||
} |
||||
|
||||
// New FileWriter A FileWriter is safe for use by multiple goroutines simultaneously.
|
||||
func New(fpath string, fns ...Option) (*FileWriter, error) { |
||||
opt := defaultOption |
||||
for _, fn := range fns { |
||||
fn(&opt) |
||||
} |
||||
|
||||
fname := filepath.Base(fpath) |
||||
if fname == "" { |
||||
return nil, fmt.Errorf("filename can't empty") |
||||
} |
||||
dir := filepath.Dir(fpath) |
||||
fi, err := os.Stat(dir) |
||||
if err == nil && !fi.IsDir() { |
||||
return nil, fmt.Errorf("%s already exists and not a directory", dir) |
||||
} |
||||
if os.IsNotExist(err) { |
||||
if err = os.MkdirAll(dir, 0755); err != nil { |
||||
return nil, fmt.Errorf("create dir %s error: %s", dir, err.Error()) |
||||
} |
||||
} |
||||
|
||||
current, err := newWrapFile(fpath) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
stdlog := log.New(os.Stderr, "flog ", log.LstdFlags) |
||||
ch := make(chan *bytes.Buffer, opt.ChanSize) |
||||
|
||||
files, err := parseRotateItem(dir, fname, opt.RotateFormat) |
||||
if err != nil { |
||||
// set files a empty list
|
||||
files = list.New() |
||||
stdlog.Printf("parseRotateItem error: %s", err) |
||||
} |
||||
|
||||
lastRotateFormat := time.Now().Format(opt.RotateFormat) |
||||
var lastSplitNum int |
||||
if files.Len() > 0 { |
||||
rt := files.Front().Value.(rotateItem) |
||||
// check contains is mush esay than compared with timestamp
|
||||
if strings.Contains(rt.fname, lastRotateFormat) { |
||||
lastSplitNum = rt.rotateNum |
||||
} |
||||
} |
||||
|
||||
fw := &FileWriter{ |
||||
opt: opt, |
||||
dir: dir, |
||||
fname: fname, |
||||
stdlog: stdlog, |
||||
ch: ch, |
||||
pool: &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}, |
||||
|
||||
lastSplitNum: lastSplitNum, |
||||
lastRotateFormat: lastRotateFormat, |
||||
|
||||
files: files, |
||||
current: current, |
||||
} |
||||
|
||||
fw.wg.Add(1) |
||||
go fw.daemon() |
||||
|
||||
return fw, nil |
||||
} |
||||
|
||||
// Write write data to log file, return write bytes is pseudo just for implement io.Writer.
|
||||
func (f *FileWriter) Write(p []byte) (int, error) { |
||||
// atomic is not necessary
|
||||
if atomic.LoadInt32(&f.closed) == 1 { |
||||
f.stdlog.Printf("%s", p) |
||||
return 0, fmt.Errorf("filewriter already closed") |
||||
} |
||||
// because write to file is asynchronousc,
|
||||
// copy p to internal buf prevent p be change on outside
|
||||
buf := f.getBuf() |
||||
buf.Write(p) |
||||
|
||||
if f.opt.WriteTimeout == 0 { |
||||
select { |
||||
case f.ch <- buf: |
||||
return len(p), nil |
||||
default: |
||||
// TODO: write discard log to to stdout?
|
||||
return 0, fmt.Errorf("log channel is full, discard log") |
||||
} |
||||
} |
||||
|
||||
// write log with timeout
|
||||
timeout := time.NewTimer(f.opt.WriteTimeout) |
||||
select { |
||||
case f.ch <- buf: |
||||
return len(p), nil |
||||
case <-timeout.C: |
||||
// TODO: write discard log to to stdout?
|
||||
return 0, fmt.Errorf("log channel is full, discard log") |
||||
} |
||||
} |
||||
|
||||
func (f *FileWriter) daemon() { |
||||
// TODO: check aggsbuf size prevent it too big
|
||||
aggsbuf := &bytes.Buffer{} |
||||
tk := time.NewTicker(f.opt.RotateInterval) |
||||
// TODO: make it configrable
|
||||
aggstk := time.NewTicker(10 * time.Millisecond) |
||||
var err error |
||||
for { |
||||
select { |
||||
case t := <-tk.C: |
||||
f.checkRotate(t) |
||||
case buf, ok := <-f.ch: |
||||
if ok { |
||||
aggsbuf.Write(buf.Bytes()) |
||||
f.putBuf(buf) |
||||
} |
||||
case <-aggstk.C: |
||||
if aggsbuf.Len() > 0 { |
||||
if err = f.write(aggsbuf.Bytes()); err != nil { |
||||
f.stdlog.Printf("write log error: %s", err) |
||||
} |
||||
aggsbuf.Reset() |
||||
} |
||||
} |
||||
if atomic.LoadInt32(&f.closed) != 1 { |
||||
continue |
||||
} |
||||
// read all buf from channel and break loop
|
||||
if err = f.write(aggsbuf.Bytes()); err != nil { |
||||
f.stdlog.Printf("write log error: %s", err) |
||||
} |
||||
for buf := range f.ch { |
||||
if err = f.write(buf.Bytes()); err != nil { |
||||
f.stdlog.Printf("write log error: %s", err) |
||||
} |
||||
f.putBuf(buf) |
||||
} |
||||
break |
||||
} |
||||
f.wg.Done() |
||||
} |
||||
|
||||
// Close close file writer
|
||||
func (f *FileWriter) Close() error { |
||||
atomic.StoreInt32(&f.closed, 1) |
||||
close(f.ch) |
||||
f.wg.Wait() |
||||
return nil |
||||
} |
||||
|
||||
func (f *FileWriter) checkRotate(t time.Time) { |
||||
formatFname := func(format string, num int) string { |
||||
if num == 0 { |
||||
return fmt.Sprintf("%s.%s", f.fname, format) |
||||
} |
||||
return fmt.Sprintf("%s.%s.%03d", f.fname, format, num) |
||||
} |
||||
format := t.Format(f.opt.RotateFormat) |
||||
|
||||
if f.opt.MaxFile != 0 { |
||||
for f.files.Len() > f.opt.MaxFile { |
||||
rt := f.files.Remove(f.files.Front()).(rotateItem) |
||||
fpath := filepath.Join(f.dir, rt.fname) |
||||
if err := os.Remove(fpath); err != nil { |
||||
f.stdlog.Printf("remove file %s error: %s", fpath, err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
if format != f.lastRotateFormat || (f.opt.MaxSize != 0 && f.current.size() > f.opt.MaxSize) { |
||||
var err error |
||||
// close current file first
|
||||
if err = f.current.fp.Close(); err != nil { |
||||
f.stdlog.Printf("close current file error: %s", err) |
||||
} |
||||
|
||||
// rename file
|
||||
fname := formatFname(f.lastRotateFormat, f.lastSplitNum) |
||||
oldpath := filepath.Join(f.dir, f.fname) |
||||
newpath := filepath.Join(f.dir, fname) |
||||
if err = os.Rename(oldpath, newpath); err != nil { |
||||
f.stdlog.Printf("rename file %s to %s error: %s", oldpath, newpath, err) |
||||
return |
||||
} |
||||
|
||||
f.files.PushBack(rotateItem{fname: fname /*rotateNum: f.lastSplitNum, rotateTime: t.Unix() unnecessary*/}) |
||||
|
||||
if format != f.lastRotateFormat { |
||||
f.lastRotateFormat = format |
||||
f.lastSplitNum = 0 |
||||
} else { |
||||
f.lastSplitNum++ |
||||
} |
||||
|
||||
// recreate current file
|
||||
f.current, err = newWrapFile(filepath.Join(f.dir, f.fname)) |
||||
if err != nil { |
||||
f.stdlog.Printf("create log file error: %s", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (f *FileWriter) write(p []byte) error { |
||||
// f.current may be nil, if newWrapFile return err in checkRotate, redirect log to stderr
|
||||
if f.current == nil { |
||||
f.stdlog.Printf("can't write log to file, please check stderr log for detail") |
||||
f.stdlog.Printf("%s", p) |
||||
} |
||||
_, err := f.current.write(p) |
||||
return err |
||||
} |
||||
|
||||
func (f *FileWriter) putBuf(buf *bytes.Buffer) { |
||||
buf.Reset() |
||||
f.pool.Put(buf) |
||||
} |
||||
|
||||
func (f *FileWriter) getBuf() *bytes.Buffer { |
||||
return f.pool.Get().(*bytes.Buffer) |
||||
} |
@ -0,0 +1,221 @@ |
||||
package filewriter |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
const logdir = "testlog" |
||||
|
||||
func touch(dir, name string) { |
||||
os.MkdirAll(dir, 0755) |
||||
fp, err := os.OpenFile(filepath.Join(dir, name), os.O_CREATE, 0644) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
fp.Close() |
||||
} |
||||
|
||||
func TestMain(m *testing.M) { |
||||
ret := m.Run() |
||||
// os.RemoveAll(logdir)
|
||||
os.Exit(ret) |
||||
} |
||||
|
||||
func TestParseRotate(t *testing.T) { |
||||
touch := func(dir, name string) { |
||||
os.MkdirAll(dir, 0755) |
||||
fp, err := os.OpenFile(filepath.Join(dir, name), os.O_CREATE, 0644) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
fp.Close() |
||||
} |
||||
dir := filepath.Join(logdir, "test-parse-rotate") |
||||
names := []string{"info.log.2018-11-11", "info.log.2018-11-11.001", "info.log.2018-11-11.002", "info.log." + time.Now().Format("2006-01-02") + ".005"} |
||||
for _, name := range names { |
||||
touch(dir, name) |
||||
} |
||||
l, err := parseRotateItem(dir, "info.log", "2006-01-02") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
assert.Equal(t, len(names), l.Len()) |
||||
|
||||
rt := l.Front().Value.(rotateItem) |
||||
|
||||
assert.Equal(t, 5, rt.rotateNum) |
||||
} |
||||
|
||||
func TestRotateExists(t *testing.T) { |
||||
dir := filepath.Join(logdir, "test-rotate-exists") |
||||
names := []string{"info.log." + time.Now().Format("2006-01-02") + ".005"} |
||||
for _, name := range names { |
||||
touch(dir, name) |
||||
} |
||||
fw, err := New(logdir+"/test-rotate-exists/info.log", |
||||
MaxSize(1024*1024), |
||||
func(opt *option) { opt.RotateInterval = time.Millisecond }, |
||||
) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
data := make([]byte, 1024) |
||||
for i := range data { |
||||
data[i] = byte(i) |
||||
} |
||||
for i := 0; i < 10; i++ { |
||||
for i := 0; i < 1024; i++ { |
||||
_, err = fw.Write(data) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
time.Sleep(10 * time.Millisecond) |
||||
} |
||||
fw.Close() |
||||
fis, err := ioutil.ReadDir(logdir + "/test-rotate-exists") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
var fnams []string |
||||
for _, fi := range fis { |
||||
fnams = append(fnams, fi.Name()) |
||||
} |
||||
assert.Contains(t, fnams, "info.log."+time.Now().Format("2006-01-02")+".006") |
||||
} |
||||
|
||||
func TestSizeRotate(t *testing.T) { |
||||
fw, err := New(logdir+"/test-rotate/info.log", |
||||
MaxSize(1024*1024), |
||||
func(opt *option) { opt.RotateInterval = 1 * time.Millisecond }, |
||||
) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
data := make([]byte, 1024) |
||||
for i := range data { |
||||
data[i] = byte(i) |
||||
} |
||||
for i := 0; i < 10; i++ { |
||||
for i := 0; i < 1024; i++ { |
||||
_, err = fw.Write(data) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
time.Sleep(10 * time.Millisecond) |
||||
} |
||||
fw.Close() |
||||
fis, err := ioutil.ReadDir(logdir + "/test-rotate") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
assert.True(t, len(fis) > 5, "expect more than 5 file get %d", len(fis)) |
||||
} |
||||
|
||||
func TestMaxFile(t *testing.T) { |
||||
fw, err := New(logdir+"/test-maxfile/info.log", |
||||
MaxSize(1024*1024), |
||||
MaxFile(1), |
||||
func(opt *option) { opt.RotateInterval = 1 * time.Millisecond }, |
||||
) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
data := make([]byte, 1024) |
||||
for i := range data { |
||||
data[i] = byte(i) |
||||
} |
||||
for i := 0; i < 10; i++ { |
||||
for i := 0; i < 1024; i++ { |
||||
_, err = fw.Write(data) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
time.Sleep(10 * time.Millisecond) |
||||
} |
||||
fw.Close() |
||||
fis, err := ioutil.ReadDir(logdir + "/test-maxfile") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
assert.True(t, len(fis) <= 2, fmt.Sprintf("expect 2 file get %d", len(fis))) |
||||
} |
||||
|
||||
func TestMaxFile2(t *testing.T) { |
||||
files := []string{ |
||||
"info.log.2018-12-01", |
||||
"info.log.2018-12-02", |
||||
"info.log.2018-12-03", |
||||
"info.log.2018-12-04", |
||||
"info.log.2018-12-05", |
||||
"info.log.2018-12-05.001", |
||||
} |
||||
for _, file := range files { |
||||
touch(logdir+"/test-maxfile2", file) |
||||
} |
||||
fw, err := New(logdir+"/test-maxfile2/info.log", |
||||
MaxSize(1024*1024), |
||||
MaxFile(3), |
||||
func(opt *option) { opt.RotateInterval = 1 * time.Millisecond }, |
||||
) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
data := make([]byte, 1024) |
||||
for i := range data { |
||||
data[i] = byte(i) |
||||
} |
||||
for i := 0; i < 10; i++ { |
||||
for i := 0; i < 1024; i++ { |
||||
_, err = fw.Write(data) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
time.Sleep(10 * time.Millisecond) |
||||
} |
||||
fw.Close() |
||||
fis, err := ioutil.ReadDir(logdir + "/test-maxfile2") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
assert.True(t, len(fis) == 4, fmt.Sprintf("expect 4 file get %d", len(fis))) |
||||
} |
||||
|
||||
func TestFileWriter(t *testing.T) { |
||||
fw, err := New("testlog/info.log") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
defer fw.Close() |
||||
_, err = fw.Write([]byte("Hello World!\n")) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
} |
||||
|
||||
func BenchmarkFileWriter(b *testing.B) { |
||||
fw, err := New("testlog/bench/info.log", |
||||
func(opt *option) { opt.WriteTimeout = time.Second }, MaxSize(1024*1024*8), /*32MB*/ |
||||
func(opt *option) { opt.RotateInterval = 10 * time.Millisecond }, |
||||
) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
for i := 0; i < b.N; i++ { |
||||
_, err = fw.Write([]byte("Hello World!\n")) |
||||
if err != nil { |
||||
b.Error(err) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,69 @@ |
||||
package filewriter |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
// RotateFormat
|
||||
const ( |
||||
RotateDaily = "2006-01-02" |
||||
) |
||||
|
||||
var defaultOption = option{ |
||||
RotateFormat: RotateDaily, |
||||
MaxSize: 1 << 30, |
||||
ChanSize: 1024 * 8, |
||||
RotateInterval: 10 * time.Second, |
||||
} |
||||
|
||||
type option struct { |
||||
RotateFormat string |
||||
MaxFile int |
||||
MaxSize int64 |
||||
ChanSize int |
||||
|
||||
// TODO export Option
|
||||
RotateInterval time.Duration |
||||
WriteTimeout time.Duration |
||||
} |
||||
|
||||
// Option filewriter option
|
||||
type Option func(opt *option) |
||||
|
||||
// RotateFormat e.g 2006-01-02 meaning rotate log file every day.
|
||||
// NOTE: format can't contain ".", "." will cause panic ヽ(*。>Д<)o゜.
|
||||
func RotateFormat(format string) Option { |
||||
if strings.Contains(format, ".") { |
||||
panic(fmt.Sprintf("rotate format can't contain '.' format: %s", format)) |
||||
} |
||||
return func(opt *option) { |
||||
opt.RotateFormat = format |
||||
} |
||||
} |
||||
|
||||
// MaxFile default 999, 0 meaning unlimit.
|
||||
// TODO: don't create file list if MaxSize is unlimt.
|
||||
func MaxFile(n int) Option { |
||||
return func(opt *option) { |
||||
opt.MaxFile = n |
||||
} |
||||
} |
||||
|
||||
// MaxSize set max size for single log file,
|
||||
// defult 1GB, 0 meaning unlimit.
|
||||
func MaxSize(n int64) Option { |
||||
return func(opt *option) { |
||||
opt.MaxSize = n |
||||
} |
||||
} |
||||
|
||||
// ChanSize set internal chan size default 8192 use about 64k memory on x64 platfrom static,
|
||||
// because filewriter has internal object pool, change chan size bigger may cause filewriter use
|
||||
// a lot of memory, because sync.Pool can't set expire time memory won't free until program exit.
|
||||
func ChanSize(n int) Option { |
||||
return func(opt *option) { |
||||
opt.ChanSize = n |
||||
} |
||||
} |
@ -0,0 +1,424 @@ |
||||
package core |
||||
|
||||
import ( |
||||
"encoding/base64" |
||||
"encoding/json" |
||||
"math" |
||||
"sync" |
||||
"time" |
||||
"unicode/utf8" |
||||
) |
||||
|
||||
// For JSON-escaping; see jsonEncoder.safeAddString below.
|
||||
const _hex = "0123456789abcdef" |
||||
|
||||
var _ ObjectEncoder = &jsonEncoder{} |
||||
var _jsonPool = sync.Pool{New: func() interface{} { |
||||
return &jsonEncoder{} |
||||
}} |
||||
|
||||
func getJSONEncoder() *jsonEncoder { |
||||
return _jsonPool.Get().(*jsonEncoder) |
||||
} |
||||
|
||||
func putJSONEncoder(enc *jsonEncoder) { |
||||
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) |
||||
} |
||||
|
||||
type jsonEncoder struct { |
||||
*EncoderConfig |
||||
buf *Buffer |
||||
spaced bool // include spaces after colons and commas
|
||||
openNamespaces int |
||||
|
||||
// for encoding generic values by reflection
|
||||
reflectBuf *Buffer |
||||
reflectEnc *json.Encoder |
||||
} |
||||
|
||||
// NewJSONEncoder creates a fast, low-allocation JSON encoder. The encoder
|
||||
// appropriately escapes all field keys and values.
|
||||
//
|
||||
// Note that the encoder doesn't deduplicate keys, so it's possible to produce
|
||||
// a message like
|
||||
// {"foo":"bar","foo":"baz"}
|
||||
// This is permitted by the JSON specification, but not encouraged. Many
|
||||
// libraries will ignore duplicate key-value pairs (typically keeping the last
|
||||
// pair) when unmarshaling, but users should attempt to avoid adding duplicate
|
||||
// keys.
|
||||
func NewJSONEncoder(cfg EncoderConfig, buf *Buffer) Encoder { |
||||
return newJSONEncoder(cfg, false, buf) |
||||
} |
||||
|
||||
func newJSONEncoder(cfg EncoderConfig, spaced bool, buf *Buffer) *jsonEncoder { |
||||
return &jsonEncoder{ |
||||
EncoderConfig: &cfg, |
||||
buf: buf, |
||||
spaced: spaced, |
||||
} |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddArray(key string, arr ArrayMarshaler) error { |
||||
enc.addKey(key) |
||||
return enc.AppendArray(arr) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddObject(key string, obj ObjectMarshaler) error { |
||||
enc.addKey(key) |
||||
return enc.AppendObject(obj) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddBinary(key string, val []byte) { |
||||
enc.AddString(key, base64.StdEncoding.EncodeToString(val)) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddByteString(key string, val []byte) { |
||||
enc.addKey(key) |
||||
enc.AppendByteString(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddBool(key string, val bool) { |
||||
enc.addKey(key) |
||||
enc.AppendBool(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddComplex128(key string, val complex128) { |
||||
enc.addKey(key) |
||||
enc.AppendComplex128(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddDuration(key string, val time.Duration) { |
||||
enc.addKey(key) |
||||
enc.AppendDuration(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddFloat64(key string, val float64) { |
||||
enc.addKey(key) |
||||
enc.AppendFloat64(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddInt64(key string, val int64) { |
||||
enc.addKey(key) |
||||
enc.AppendInt64(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) resetReflectBuf() { |
||||
if enc.reflectBuf == nil { |
||||
enc.reflectBuf = GetPool() |
||||
enc.reflectEnc = json.NewEncoder(enc.reflectBuf) |
||||
} else { |
||||
enc.reflectBuf.Reset() |
||||
} |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddReflected(key string, obj interface{}) error { |
||||
enc.resetReflectBuf() |
||||
err := enc.reflectEnc.Encode(obj) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
enc.reflectBuf.TrimNewline() |
||||
enc.addKey(key) |
||||
_, err = enc.buf.Write(enc.reflectBuf.Bytes()) |
||||
return err |
||||
} |
||||
|
||||
func (enc *jsonEncoder) OpenNamespace(key string) { |
||||
enc.addKey(key) |
||||
enc.buf.AppendByte('{') |
||||
enc.openNamespaces++ |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddString(key, val string) { |
||||
enc.addKey(key) |
||||
enc.AppendString(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddTime(key string, val time.Time) { |
||||
enc.addKey(key) |
||||
enc.AppendTime(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddUint64(key string, val uint64) { |
||||
enc.addKey(key) |
||||
enc.AppendUint64(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendArray(arr ArrayMarshaler) error { |
||||
enc.addElementSeparator() |
||||
enc.buf.AppendByte('[') |
||||
err := arr.MarshalLogArray(enc) |
||||
enc.buf.AppendByte(']') |
||||
return err |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendObject(obj ObjectMarshaler) error { |
||||
enc.addElementSeparator() |
||||
enc.buf.AppendByte('{') |
||||
err := obj.MarshalLogObject(enc) |
||||
enc.buf.AppendByte('}') |
||||
return err |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendBool(val bool) { |
||||
enc.addElementSeparator() |
||||
enc.buf.AppendBool(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendByteString(val []byte) { |
||||
enc.addElementSeparator() |
||||
enc.buf.AppendByte('"') |
||||
enc.safeAddByteString(val) |
||||
enc.buf.AppendByte('"') |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendComplex128(val complex128) { |
||||
enc.addElementSeparator() |
||||
// Cast to a platform-independent, fixed-size type.
|
||||
r, i := float64(real(val)), float64(imag(val)) |
||||
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, 64) |
||||
enc.buf.AppendByte('+') |
||||
enc.buf.AppendFloat(i, 64) |
||||
enc.buf.AppendByte('i') |
||||
enc.buf.AppendByte('"') |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendDuration(val time.Duration) { |
||||
cur := enc.buf.Len() |
||||
enc.EncodeDuration(val, enc) |
||||
if cur == enc.buf.Len() { |
||||
// User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep
|
||||
// JSON valid.
|
||||
enc.AppendInt64(int64(val)) |
||||
} |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendInt64(val int64) { |
||||
enc.addElementSeparator() |
||||
enc.buf.AppendInt(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendReflected(val interface{}) error { |
||||
enc.resetReflectBuf() |
||||
err := enc.reflectEnc.Encode(val) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
enc.reflectBuf.TrimNewline() |
||||
enc.addElementSeparator() |
||||
_, err = enc.buf.Write(enc.reflectBuf.Bytes()) |
||||
return err |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendString(val string) { |
||||
enc.addElementSeparator() |
||||
enc.buf.AppendByte('"') |
||||
enc.safeAddString(val) |
||||
enc.buf.AppendByte('"') |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendTime(val time.Time) { |
||||
cur := enc.buf.Len() |
||||
enc.EncodeTime(val, enc) |
||||
if cur == enc.buf.Len() { |
||||
// User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep
|
||||
// output JSON valid.
|
||||
enc.AppendInt64(val.UnixNano()) |
||||
} |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AppendUint64(val uint64) { |
||||
enc.addElementSeparator() |
||||
enc.buf.AppendUint(val) |
||||
} |
||||
|
||||
func (enc *jsonEncoder) AddComplex64(k string, v complex64) { enc.AddComplex128(k, complex128(v)) } |
||||
func (enc *jsonEncoder) AddFloat32(k string, v float32) { enc.AddFloat64(k, float64(v)) } |
||||
func (enc *jsonEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) } |
||||
func (enc *jsonEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) } |
||||
func (enc *jsonEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) } |
||||
func (enc *jsonEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) } |
||||
func (enc *jsonEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) } |
||||
func (enc *jsonEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) } |
||||
func (enc *jsonEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) } |
||||
func (enc *jsonEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) } |
||||
func (enc *jsonEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) } |
||||
func (enc *jsonEncoder) AppendComplex64(v complex64) { enc.AppendComplex128(complex128(v)) } |
||||
func (enc *jsonEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) } |
||||
func (enc *jsonEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) } |
||||
func (enc *jsonEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) } |
||||
func (enc *jsonEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) } |
||||
func (enc *jsonEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) } |
||||
func (enc *jsonEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) } |
||||
func (enc *jsonEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) } |
||||
func (enc *jsonEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) } |
||||
func (enc *jsonEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) } |
||||
func (enc *jsonEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) } |
||||
func (enc *jsonEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) } |
||||
|
||||
func (enc *jsonEncoder) Clone() Encoder { |
||||
clone := enc.clone() |
||||
return clone |
||||
} |
||||
|
||||
func (enc *jsonEncoder) clone() *jsonEncoder { |
||||
clone := getJSONEncoder() |
||||
clone.EncoderConfig = enc.EncoderConfig |
||||
clone.spaced = enc.spaced |
||||
clone.openNamespaces = enc.openNamespaces |
||||
clone.buf = GetPool() |
||||
return clone |
||||
} |
||||
|
||||
func (enc *jsonEncoder) Encode(buf *Buffer, fields ...Field) error { |
||||
final := enc.clone() |
||||
final.buf = buf |
||||
final.buf.AppendByte('{') |
||||
if enc.buf.Len() > 0 { |
||||
final.addElementSeparator() |
||||
final.buf.Write(enc.buf.Bytes()) |
||||
} |
||||
|
||||
for i := range fields { |
||||
fields[i].AddTo(final) |
||||
} |
||||
|
||||
final.closeOpenNamespaces() |
||||
final.buf.AppendString("}\n") |
||||
putJSONEncoder(final) |
||||
return nil |
||||
} |
||||
|
||||
func (enc *jsonEncoder) closeOpenNamespaces() { |
||||
for i := 0; i < enc.openNamespaces; i++ { |
||||
enc.buf.AppendByte('}') |
||||
} |
||||
} |
||||
|
||||
func (enc *jsonEncoder) addKey(key string) { |
||||
enc.addElementSeparator() |
||||
enc.buf.AppendByte('"') |
||||
enc.safeAddString(key) |
||||
enc.buf.AppendByte('"') |
||||
enc.buf.AppendByte(':') |
||||
if enc.spaced { |
||||
enc.buf.AppendByte(' ') |
||||
} |
||||
} |
||||
|
||||
func (enc *jsonEncoder) 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 *jsonEncoder) 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: |
||||
enc.buf.AppendFloat(val, bitSize) |
||||
} |
||||
} |
||||
|
||||
// safeAddString JSON-escapes a string and appends it to the internal buffer.
|
||||
// Unlike the standard library's encoder, it doesn't attempt to protect the
|
||||
// user from browser vulnerabilities or JSONP-related problems.
|
||||
func (enc *jsonEncoder) 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 |
||||
} |
||||
} |
||||
|
||||
// safeAddByteString is no-alloc equivalent of safeAddString(string(s)) for s []byte.
|
||||
func (enc *jsonEncoder) 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 |
||||
} |
||||
} |
||||
|
||||
// tryAddRuneSelf appends b if it is valid UTF-8 character represented in a single byte.
|
||||
func (enc *jsonEncoder) 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 *jsonEncoder) tryAddRuneError(r rune, size int) bool { |
||||
if r == utf8.RuneError && size == 1 { |
||||
enc.buf.AppendString(`\ufffd`) |
||||
return true |
||||
} |
||||
return false |
||||
} |
@ -0,0 +1,32 @@ |
||||
package core |
||||
|
||||
import "sync" |
||||
|
||||
// A Pool is a type-safe wrapper around a sync.Pool.
|
||||
type Pool struct { |
||||
p *sync.Pool |
||||
} |
||||
|
||||
// NewPool constructs a new Pool.
|
||||
func NewPool(size int) Pool { |
||||
if size == 0 { |
||||
size = _size |
||||
} |
||||
return Pool{p: &sync.Pool{ |
||||
New: func() interface{} { |
||||
return &Buffer{bs: make([]byte, 0, size)} |
||||
}, |
||||
}} |
||||
} |
||||
|
||||
// Get retrieves a Buffer from the pool, creating one if necessary.
|
||||
func (p Pool) Get() *Buffer { |
||||
buf := p.p.Get().(*Buffer) |
||||
buf.Reset() |
||||
buf.pool = p |
||||
return buf |
||||
} |
||||
|
||||
func (p Pool) put(buf *Buffer) { |
||||
p.p.Put(buf) |
||||
} |
@ -0,0 +1,32 @@ |
||||
package core |
||||
|
||||
import ( |
||||
"sync" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestBuffers(t *testing.T) { |
||||
const dummyData = "dummy data" |
||||
p := NewPool(0) |
||||
|
||||
var wg sync.WaitGroup |
||||
for g := 0; g < 10; g++ { |
||||
wg.Add(1) |
||||
go func() { |
||||
for i := 0; i < 100; i++ { |
||||
buf := p.Get() |
||||
assert.Zero(t, buf.Len(), "Expected truncated buffer") |
||||
assert.NotZero(t, buf.Cap(), "Expected non-zero capacity") |
||||
|
||||
buf.AppendString(dummyData) |
||||
assert.Equal(t, buf.Len(), len(dummyData), "Expected buffer to contain dummy data") |
||||
|
||||
buf.Free() |
||||
} |
||||
wg.Done() |
||||
}() |
||||
} |
||||
wg.Wait() |
||||
} |
@ -0,0 +1,89 @@ |
||||
package log |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/bilibili/Kratos/pkg/net/metadata" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func initStdout() { |
||||
conf := &Config{ |
||||
Stdout: true, |
||||
} |
||||
Init(conf) |
||||
} |
||||
|
||||
func initFile() { |
||||
conf := &Config{ |
||||
Dir: "/tmp", |
||||
// VLevel: 2,
|
||||
Module: map[string]int32{"log_test": 1}, |
||||
} |
||||
Init(conf) |
||||
} |
||||
|
||||
type TestLog struct { |
||||
A string |
||||
B int |
||||
C string |
||||
D string |
||||
} |
||||
|
||||
func testLog(t *testing.T) { |
||||
t.Run("Error", func(t *testing.T) { |
||||
Error("hello %s", "world") |
||||
Errorv(context.Background(), KV("key", 2222222), KV("test2", "test")) |
||||
Errorc(context.Background(), "keys: %s %s...", "key1", "key2") |
||||
Errorw(context.Background(), "key1", "value1", "key2", "value2") |
||||
}) |
||||
t.Run("Warn", func(t *testing.T) { |
||||
Warn("hello %s", "world") |
||||
Warnv(context.Background(), KV("key", 2222222), KV("test2", "test")) |
||||
Warnc(context.Background(), "keys: %s %s...", "key1", "key2") |
||||
Warnw(context.Background(), "key1", "value1", "key2", "value2") |
||||
}) |
||||
t.Run("Info", func(t *testing.T) { |
||||
Info("hello %s", "world") |
||||
Infov(context.Background(), KV("key", 2222222), KV("test2", "test")) |
||||
Infoc(context.Background(), "keys: %s %s...", "key1", "key2") |
||||
Infow(context.Background(), "key1", "value1", "key2", "value2") |
||||
}) |
||||
} |
||||
|
||||
func TestFile(t *testing.T) { |
||||
initFile() |
||||
testLog(t) |
||||
assert.Equal(t, nil, Close()) |
||||
} |
||||
|
||||
func TestStdout(t *testing.T) { |
||||
initStdout() |
||||
testLog(t) |
||||
assert.Equal(t, nil, Close()) |
||||
} |
||||
|
||||
func TestLogW(t *testing.T) { |
||||
D := logw([]interface{}{"i", "like", "a", "dog"}) |
||||
if len(D) != 2 || D[0].Key != "i" || D[0].Value != "like" || D[1].Key != "a" || D[1].Value != "dog" { |
||||
t.Fatalf("logw out put should be ' {i like} {a dog}'") |
||||
} |
||||
D = logw([]interface{}{"i", "like", "dog"}) |
||||
if len(D) != 1 || D[0].Key != "i" || D[0].Value != "like" { |
||||
t.Fatalf("logw out put should be ' {i like}'") |
||||
} |
||||
} |
||||
|
||||
func TestLogWithMirror(t *testing.T) { |
||||
Info("test log") |
||||
mdcontext := metadata.NewContext(context.Background(), metadata.MD{metadata.Mirror: "true"}) |
||||
Infov(mdcontext, KV("key1", "val1"), KV("key2", ""), KV("log", "log content"), KV("msg", "msg content")) |
||||
|
||||
mdcontext = metadata.NewContext(context.Background(), metadata.MD{metadata.Mirror: "***"}) |
||||
Infov(mdcontext, KV("key1", "val1"), KV("key2", ""), KV("log", "log content"), KV("msg", "msg content")) |
||||
|
||||
Infov(context.Background(), KV("key1", "val1"), KV("key2", ""), KV("log", "log content"), KV("msg", "msg content")) |
||||
|
||||
} |
@ -0,0 +1,173 @@ |
||||
package log |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"io" |
||||
"path" |
||||
"runtime" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
) |
||||
|
||||
// Render render log output
|
||||
type Render interface { |
||||
Render(io.Writer, map[string]interface{}) error |
||||
RenderString(map[string]interface{}) string |
||||
} |
||||
|
||||
var patternMap = map[string]func(map[string]interface{}) string{ |
||||
"T": longTime, |
||||
"t": shortTime, |
||||
"D": longDate, |
||||
"d": shortDate, |
||||
"L": keyFactory(_level), |
||||
"f": keyFactory(_source), |
||||
"i": keyFactory(_instanceID), |
||||
"e": keyFactory(_deplyEnv), |
||||
"z": keyFactory(_zone), |
||||
"S": longSource, |
||||
"s": shortSource, |
||||
"M": message, |
||||
} |
||||
|
||||
// newPatternRender new pattern render
|
||||
func newPatternRender(format string) Render { |
||||
p := &pattern{ |
||||
bufPool: sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}, |
||||
} |
||||
b := make([]byte, 0, len(format)) |
||||
for i := 0; i < len(format); i++ { |
||||
if format[i] != '%' { |
||||
b = append(b, format[i]) |
||||
continue |
||||
} |
||||
if i+1 >= len(format) { |
||||
b = append(b, format[i]) |
||||
continue |
||||
} |
||||
f, ok := patternMap[string(format[i+1])] |
||||
if !ok { |
||||
b = append(b, format[i]) |
||||
continue |
||||
} |
||||
if len(b) != 0 { |
||||
p.funcs = append(p.funcs, textFactory(string(b))) |
||||
b = b[:0] |
||||
} |
||||
p.funcs = append(p.funcs, f) |
||||
i++ |
||||
} |
||||
if len(b) != 0 { |
||||
p.funcs = append(p.funcs, textFactory(string(b))) |
||||
} |
||||
return p |
||||
} |
||||
|
||||
type pattern struct { |
||||
funcs []func(map[string]interface{}) string |
||||
bufPool sync.Pool |
||||
} |
||||
|
||||
// Render implemet Formater
|
||||
func (p *pattern) Render(w io.Writer, d map[string]interface{}) error { |
||||
buf := p.bufPool.Get().(*bytes.Buffer) |
||||
defer func() { |
||||
buf.Reset() |
||||
p.bufPool.Put(buf) |
||||
}() |
||||
for _, f := range p.funcs { |
||||
buf.WriteString(f(d)) |
||||
} |
||||
|
||||
_, err := buf.WriteTo(w) |
||||
return err |
||||
} |
||||
|
||||
// Render implemet Formater as string
|
||||
func (p *pattern) RenderString(d map[string]interface{}) string { |
||||
// TODO strings.Builder
|
||||
buf := p.bufPool.Get().(*bytes.Buffer) |
||||
defer func() { |
||||
buf.Reset() |
||||
p.bufPool.Put(buf) |
||||
}() |
||||
for _, f := range p.funcs { |
||||
buf.WriteString(f(d)) |
||||
} |
||||
|
||||
return buf.String() |
||||
} |
||||
|
||||
func textFactory(text string) func(map[string]interface{}) string { |
||||
return func(map[string]interface{}) string { |
||||
return text |
||||
} |
||||
} |
||||
func keyFactory(key string) func(map[string]interface{}) string { |
||||
return func(d map[string]interface{}) string { |
||||
if v, ok := d[key]; ok { |
||||
if s, ok := v.(string); ok { |
||||
return s |
||||
} |
||||
return fmt.Sprint(v) |
||||
} |
||||
return "" |
||||
} |
||||
} |
||||
|
||||
func longSource(map[string]interface{}) string { |
||||
if _, file, lineNo, ok := runtime.Caller(6); ok { |
||||
return fmt.Sprintf("%s:%d", file, lineNo) |
||||
} |
||||
return "unknown:0" |
||||
} |
||||
|
||||
func shortSource(map[string]interface{}) string { |
||||
if _, file, lineNo, ok := runtime.Caller(6); ok { |
||||
return fmt.Sprintf("%s:%d", path.Base(file), lineNo) |
||||
} |
||||
return "unknown:0" |
||||
} |
||||
|
||||
func longTime(map[string]interface{}) string { |
||||
return time.Now().Format("15:04:05.000") |
||||
} |
||||
|
||||
func shortTime(map[string]interface{}) string { |
||||
return time.Now().Format("15:04") |
||||
} |
||||
|
||||
func longDate(map[string]interface{}) string { |
||||
return time.Now().Format("2006/01/02") |
||||
} |
||||
|
||||
func shortDate(map[string]interface{}) string { |
||||
return time.Now().Format("01/02") |
||||
} |
||||
|
||||
func isInternalKey(k string) bool { |
||||
switch k { |
||||
case _level, _levelValue, _time, _source, _instanceID, _appID, _deplyEnv, _zone: |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func message(d map[string]interface{}) string { |
||||
var m string |
||||
var s []string |
||||
for k, v := range d { |
||||
if k == _log { |
||||
m = fmt.Sprint(v) |
||||
continue |
||||
} |
||||
if isInternalKey(k) { |
||||
continue |
||||
} |
||||
s = append(s, fmt.Sprintf("%s=%v", k, v)) |
||||
} |
||||
s = append(s, m) |
||||
return strings.Join(s, " ") |
||||
} |
@ -0,0 +1,35 @@ |
||||
package log |
||||
|
||||
import ( |
||||
"bytes" |
||||
"strings" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func TestPatternDefault(t *testing.T) { |
||||
buf := &bytes.Buffer{} |
||||
p := newPatternRender("%L %T %f %M") |
||||
p.Render(buf, map[string]interface{}{_level: _infoLevel.String(), _log: "hello", _time: time.Now().Format(_timeFormat), _source: "xxx:123"}) |
||||
|
||||
fields := strings.Fields(buf.String()) |
||||
assert.Equal(t, 4, len(fields)) |
||||
assert.Equal(t, "INFO", fields[0]) |
||||
assert.Equal(t, "hello", fields[3]) |
||||
} |
||||
|
||||
func TestKV(t *testing.T) { |
||||
buf := &bytes.Buffer{} |
||||
p := newPatternRender("%M") |
||||
p.Render(buf, map[string]interface{}{_level: _infoLevel.String(), _log: "2233", "hello": "test"}) |
||||
assert.Equal(t, "hello=test 2233", buf.String()) |
||||
} |
||||
|
||||
func TestBadSymbol(t *testing.T) { |
||||
buf := &bytes.Buffer{} |
||||
p := newPatternRender("%12 %% %xd %M") |
||||
p.Render(buf, map[string]interface{}{_level: _infoLevel.String(), _log: "2233"}) |
||||
assert.Equal(t, "%12 %% %xd 2233", buf.String()) |
||||
} |
@ -0,0 +1,61 @@ |
||||
package log |
||||
|
||||
import ( |
||||
"context" |
||||
"io" |
||||
"os" |
||||
"time" |
||||
) |
||||
|
||||
const defaultPattern = "%L %d-%T %f %M" |
||||
|
||||
var _defaultStdout = NewStdout() |
||||
|
||||
// StdoutHandler stdout log handler
|
||||
type StdoutHandler struct { |
||||
out io.Writer |
||||
render Render |
||||
} |
||||
|
||||
// NewStdout create a stdout log handler
|
||||
func NewStdout() *StdoutHandler { |
||||
return &StdoutHandler{ |
||||
out: os.Stderr, |
||||
render: newPatternRender(defaultPattern), |
||||
} |
||||
} |
||||
|
||||
// Log stdout loging, only for developing env.
|
||||
func (h *StdoutHandler) Log(ctx context.Context, lv Level, args ...D) { |
||||
d := make(map[string]interface{}, 10+len(args)) |
||||
for _, arg := range args { |
||||
d[arg.Key] = arg.Value |
||||
} |
||||
// add extra fields
|
||||
addExtraField(ctx, d) |
||||
d[_time] = time.Now().Format(_timeFormat) |
||||
h.render.Render(h.out, d) |
||||
h.out.Write([]byte("\n")) |
||||
} |
||||
|
||||
// Close stdout loging
|
||||
func (h *StdoutHandler) Close() error { |
||||
return nil |
||||
} |
||||
|
||||
// SetFormat set stdout log output format
|
||||
// %T time format at "15:04:05.999"
|
||||
// %t time format at "15:04:05"
|
||||
// %D data format at "2006/01/02"
|
||||
// %d data format at "01/02"
|
||||
// %L log level e.g. INFO WARN ERROR
|
||||
// %f function name and line number e.g. model.Get:121
|
||||
// %i instance id
|
||||
// %e deploy env e.g. dev uat fat prod
|
||||
// %z zone
|
||||
// %S full file name and line number: /a/b/c/d.go:23
|
||||
// %s final file name element and line number: d.go:23
|
||||
// %M log message and additional fields: key=value this is log message
|
||||
func (h *StdoutHandler) SetFormat(format string) { |
||||
h.render = newPatternRender(format) |
||||
} |
@ -0,0 +1,54 @@ |
||||
package log |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"runtime" |
||||
"strconv" |
||||
"sync" |
||||
|
||||
"github.com/bilibili/Kratos/pkg/conf/env" |
||||
"github.com/bilibili/Kratos/pkg/net/metadata" |
||||
"github.com/bilibili/Kratos/pkg/net/trace" |
||||
) |
||||
|
||||
var fm sync.Map |
||||
|
||||
func addExtraField(ctx context.Context, fields map[string]interface{}) { |
||||
if t, ok := trace.FromContext(ctx); ok { |
||||
if s, ok := t.(fmt.Stringer); ok { |
||||
fields[_tid] = s.String() |
||||
} else { |
||||
fields[_tid] = fmt.Sprintf("%s", t) |
||||
} |
||||
} |
||||
if caller := metadata.String(ctx, metadata.Caller); caller != "" { |
||||
fields[_caller] = caller |
||||
} |
||||
if color := metadata.String(ctx, metadata.Color); color != "" { |
||||
fields[_color] = color |
||||
} |
||||
if cluster := metadata.String(ctx, metadata.Cluster); cluster != "" { |
||||
fields[_cluster] = cluster |
||||
} |
||||
fields[_deplyEnv] = env.DeployEnv |
||||
fields[_zone] = env.Zone |
||||
fields[_appID] = c.Family |
||||
fields[_instanceID] = c.Host |
||||
if metadata.Bool(ctx, metadata.Mirror) { |
||||
fields[_mirror] = true |
||||
} |
||||
} |
||||
|
||||
// funcName get func name.
|
||||
func funcName(skip int) (name string) { |
||||
if pc, _, lineNo, ok := runtime.Caller(skip); ok { |
||||
if v, ok := fm.Load(pc); ok { |
||||
name = v.(string) |
||||
} else { |
||||
name = runtime.FuncForPC(pc).Name() + ":" + strconv.FormatInt(int64(lineNo), 10) |
||||
fm.Store(pc, name) |
||||
} |
||||
} |
||||
return |
||||
} |
Loading…
Reference in new issue