# wtflog **Repository Path**: FlyingOnion/wtflog ## Basic Information - **Project Name**: wtflog - **Description**: a logging library that provides a convenient and efficient way to log complex structures. - **Primary Language**: Go - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-05-12 - **Last Updated**: 2024-05-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Wtflog Wtflog is a logging library that provides convenient logging APIs like standard `log` and `logrus`, but more efficient in recording complex structures (with gen-bsa). ## Usage ### Basic ```go package main import "gitee.com/FlyingOnion/wtflog" func main() { logger := wtflog.NewTextLogger() logger.Info("hello world") // output: // hello world } ``` ### Struct, Map, Slice For complex structures (struct, map, slice), log them with `gen-bsa`: 1. `go install gitee.com/FlyingOnion/gen-bsa` 2. cd to root directory and run `gen-bsa`, and the code generator will generate helper functions `AppendTo` in wtf__.go file 3. use logging functions ```go type FooStruct struct { S string I int } type FooSlice []FooStruct type FooMap map[string]FooStruct ``` Part of generated code: ```go func (p FooStruct) AppendTo(b *ByteSlice) { var any interface{} _ = any b.AppendString("{S:") b.AppendString(p.S) b.AppendString(" I:") b.AppendInt64(int64(p.I)) b.AppendByte('}') } func (p FooSlice) AppendTo(b *ByteSlice) { if p == nil { b.AppendString(""); return } var any interface{} _ = any b.AppendByte('[') for i, elem := range p { if i > 0 { b.AppendByte(' ') } any = elem if bsa, ok := any.(ByteSliceAppender); ok { bsa.AppendTo(b) } else if str, ok := any.(Stringer); ok { b.AppendString(str.String()) } else { b.AppendString(fmtPrint(any)) } } b.AppendByte(']') } func (p FooMap) AppendTo(b *ByteSlice) { if p == nil { b.AppendString(""); return } if len(p) == 0 { b.AppendString("{}"); return } var any interface{} _ = any b.AppendByte('{') kvs := make([]struct{k string; v interface{}}, 0, len(p)) for k, v := range p { kvs = append(kvs, struct{k string; v interface{}}{k: string(k), v: interface{}(v)}) } sortKeys(kvs, func(i, j int) bool { return kvs[i].k < kvs[j].k }) for i, kv := range kvs { if i > 0 { b.AppendByte(' ') } b.AppendString(kv.k) b.AppendByte(':') any = kv.v if bsa, ok := any.(ByteSliceAppender); ok { bsa.AppendTo(b) } else if str, ok := any.(Stringer); ok { b.AppendString(str.String()) } else { b.AppendString(fmtPrint(any)) } } b.AppendByte('}') } ``` ```go package main import "gitee.com/FlyingOnion/wtflog" func main() { foo := FooStruct{S: "hello", I: 8} fooSlice := FooSlice([]FooStruct{foo}) fooMap := FooMap(map[string]FooStruct{"foo": foo}) l := wtflog.NewTextLogger() l.Info(foo, fooSlice, fooMap) // output: // {S:hello I:8} [{S:hello I:8}] {foo:{S:hello I:8}} } ``` ### With Field Log Level, Time, and Caller We provide `WithFieldsAtFront` and `WithFieldsAtBack` function if you want to add fields. Available fields are: `FieldCaller`, `FieldLevel`, `FieldTime`. All fields are formatted in default way `"key=value"` ```go l := wtflog.NewTextLogger().WithFieldsAtFront(FieldLevel, FieldTime).WithFieldsAtBack(FieldCaller) l.Info("hello world") // output: // level=info time=2022-05-23 10:03:21 hello world caller=c:/Users/Administrator/go/src/.../logger_test.go:22 ``` ### Logger Initialization We suggest you init and set global logger if the logger is used widely in your program. ```go import "gitee.com/FlyingOnion/wtflog" func InitLogger() { // Available options: // WithFieldsAtFront // WithFieldsAtBack // WithCallerSkip logger := wtflog.NewTextLogger(). WithLogLevel(wtflog.LevelDebug). WithWriter(yourWriter) wtflog.SetGlobalLogger(logger) // you can use wtflog.Info, wtflog.Error directly with your custom logger now. } ``` ## Work Progress - text logger: 80% - json logger: 80% - formatted logger (`f` methods): TODO - code generator gen-bsa: 90% ## Why Another Logging Library We researched current logging libraries (including popular `logrus`, `go-kit/log`, `zap`, `zerolog`), and found a disappointing phenomenon. Almost all libraries (even those claiming zero allocations) failed to find a more effective way to record complex structures (`struct`, `map`, `slice`, `array`, `pointer`). All they do is to pass the arguments to `fmt.Sprint` or `fmt.Sprintf`. In wtflog, things will be changed. The design philosophy of wtflog is to move the place that decides logging format of a variable from `fmt` to the variable itself as much as possible. This method is quite different from convention, and you may need to take some time to adapt it. ```go import "gitee.com/FlyingOnion/wtflog" import . "gitee.com/FlyingOnion/wtflog/types" func Log() { logger := wtflog.NewTextLogger() logger.Info("Hello world") logger.Info(1) // bad logger.Info(Int(1)) // good logger.Info(time.Now()) // bad logger.Info(Time(time.Now())) // good } ``` In the example above, `1` is an integer and it does not bring any information about how it will be logged (it will be decided by `fmt`). As a contrast, `Int` type is a custom type that brings these information. See the declaration of `Int`: ```go type Int int func (i Int) AppendTo(b *ByteSlice) { b.AppendInt64(int64(i)) } func (i Int) String() string { return strconv.FormatInt(int64(i), 10) } ``` The `AppendTo` function implements `ByteSliceAppender` interface, which could be distinguished by wtflog, so that it could skip `fmt` type check and directly append to logging buffer `b`. This method is similar with what `zap` and `zerolog` do. The type-check process of wtflog is quite simple. Suppose the argument is `v` (`interface{}`) 1. If `v` is nil, write ``. 2. Check whether `v` is a `[]byte`, `string`, `ByteSliceAppender`, `Stringer` successively. If matched then use corresponding write method. 3. If either is not matched use `fmt.Sprintf("%+v", v)` and write that string. We imagine that if each argument is either a `[]byte`, `string`, `ByteSliceAppender`, `Stringer`, the logging efficiency will be improved dramatically, compared with standard `log` and `logrus`. For basic types (except `string` and `[]byte`), we provide the `Int` and `Uint` family, `Float`, `Time`, and `Duration`. ```go // Assume that you have already // import . "gitee.com/FlyingOnion/wtflog/types" logger.Info(Int(1)) // or Int8(1), Int16(1), Int32(1), Int64(1) logger.Info(Uint(1)) // or Uint8(1), Uint16(1), Uint32(1), Uint64(1) logger.Info(Float32(1.0)) // or Float64(1.0) logger.Info(Float32f(1.0, 'e', 2)) // or Float64f with format and precision logger.Info(Time(time.Now())) // "2006-01-02 15:04:05" format logger.Info(Timef(time.Now(), time.RFC3339)) // RFC3339 format logger.Info(Timestamp(time.Now())) // or TimestampMilli, TimestampMicro, TimestampNano logger.Info(Duration(1*time.Second)) logger.Info(Error(io.EOF)) ``` ## Level Writer The concept of level writer is similar with the one of logrus. We provide a builtin level writer. Usage of level writer is shown below. ```go // set os.Stdout as default writer. // set a level writer map. levelWriter := NewLevelWriter(os.Stdout, map[wtflog.Level]io.Writer{ wtflog.LevelWarn: os.Stderr, wtflog.LevelError: os.Stderr, }) logger := NewTextLogger().WithWriter(levelWriter) ```