diff --git a/go.mod b/go.mod index 0434181..b248c3c 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,18 @@ go 1.21.0 require ( github.com/BurntSushi/toml v1.3.2 github.com/andybalholm/brotli v1.0.5 + github.com/coreos/go-iptables v0.7.0 github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a + github.com/gookit/color v1.5.4 github.com/klauspost/compress v1.16.7 github.com/robfig/cron v1.2.0 github.com/sirupsen/logrus v1.9.3 + go.uber.org/zap v1.25.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/coreos/go-iptables v0.7.0 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sys v0.10.0 // indirect ) diff --git a/go.sum b/go.sum index b1d50ce..8370610 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8= github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -9,6 +11,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -18,10 +22,20 @@ github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfm github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/next/logger/encoder.go b/next/logger/encoder.go new file mode 100644 index 0000000..b9eda34 --- /dev/null +++ b/next/logger/encoder.go @@ -0,0 +1,99 @@ +package logger + +import ( + "bytes" + "fmt" + "github.com/gookit/color" + "go.uber.org/zap/zapcore" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" +) + +// getGid get goroutine ID only for debugging. +// -> https://blog.sgmansfield.com/2015/12/goroutine-ids/ +func getGid() uint64 { + b := make([]byte, 64) + b = b[:runtime.Stack(b, false)] + b = bytes.TrimPrefix(b, []byte("goroutine ")) + b = b[:bytes.IndexByte(b, ' ')] + n, _ := strconv.ParseUint(string(b), 10, 64) + return n +} + +// getCaller calculate relative source path of caller. +func getCaller(ec zapcore.EntryCaller, verbose bool) string { + file, err := filepath.Rel(logHandle.path, ec.File) + if err != nil { + return "undefined" + } + if verbose { + return file + ":" + strconv.Itoa(ec.Line) + } + file, _ = strings.CutSuffix(file, ".go") // remove `.go` suffix + return file +} + +// timeEncoder formats the time as a string. +func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format("2006-01-02 15:04:05.000")) +} + +// timeColoredEncoder formats the time as a colored string +// with custom prefix. +func timeColoredEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(fmt.Sprintf( + "%s %s", + color.Cyan.Render(logHandle.prefix), // colored prefix + color.Gray.Render(t.Format("2006-01-02 15:04:05.000")), + )) +} + +// callerEncoder formats caller in square brackets. +func callerEncoder(ec zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { + if !logHandle.verbose { + enc.AppendString("[" + getCaller(ec, false) + "]") + return + } + enc.AppendString(fmt.Sprintf("[%d] [%s]", getGid(), getCaller(ec, true))) +} + +// callerColoredEncoder formats caller in square brackets with +// magenta color. +func callerColoredEncoder(ec zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { + if !logHandle.verbose { + enc.AppendString(color.Magenta.Render("[" + getCaller(ec, false) + "]")) + return + } + enc.AppendString(fmt.Sprintf( + "%s %s", + color.Blue.Render(fmt.Sprintf("[%d]", getGid())), + color.Magenta.Render("["+getCaller(ec, true)+"]"), + )) +} + +// levelEncoder formats log level using square brackets. +func levelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString("[" + level.CapitalString() + "]") +} + +// levelColoredEncoder formats log level using square brackets +// and uses different colors. +func levelColoredEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + levelStr := "[" + level.CapitalString() + "]" + switch level { + case zapcore.DebugLevel: + levelStr = color.FgDefault.Render(levelStr) + case zapcore.InfoLevel: + levelStr = color.Green.Render(levelStr) + case zapcore.WarnLevel: + levelStr = color.Yellow.Render(levelStr) + case zapcore.ErrorLevel: + levelStr = color.Red.Render(levelStr) + case zapcore.PanicLevel: + levelStr = color.LightRed.Render(levelStr) + } + enc.AppendString(levelStr) +} diff --git a/next/logger/interface.go b/next/logger/interface.go new file mode 100644 index 0000000..f58b811 --- /dev/null +++ b/next/logger/interface.go @@ -0,0 +1,59 @@ +package logger + +import ( + "go.uber.org/zap/zapcore" + "io" +) + +const ( + DebugLevel = zapcore.DebugLevel + InfoLevel = zapcore.InfoLevel + WarnLevel = zapcore.WarnLevel + ErrorLevel = zapcore.ErrorLevel + PanicLevel = zapcore.PanicLevel +) + +func Debugf(template string, args ...interface{}) { + logHandle.sugar.Debugf(template, args...) +} + +func Infof(template string, args ...interface{}) { + logHandle.sugar.Infof(template, args...) +} + +func Warnf(template string, args ...interface{}) { + logHandle.sugar.Warnf(template, args...) +} + +func Errorf(template string, args ...interface{}) { + logHandle.sugar.Errorf(template, args...) +} + +func Panicf(template string, args ...interface{}) { + logHandle.sugar.Panicf(template, args...) +} + +// GetLevel return the current logger level. +func GetLevel() zapcore.Level { + return logHandle.level.Level() +} + +// SetLevel configure logger output level. Note that debug level +// will output more information and reduce performance. +func SetLevel(level zapcore.Level) { + logHandle.level.SetLevel(level) + if level == DebugLevel { + logHandle.verbose = true + } else { + logHandle.verbose = false + } +} + +// AddOutputs adds more plain output channel to the logger module. +func AddOutputs(outputs ...io.Writer) { + var writers []zapcore.WriteSyncer + for _, output := range outputs { + writers = append(writers, zapcore.AddSync(output)) + } + addWrites(writers...) +} diff --git a/next/logger/logger.go b/next/logger/logger.go new file mode 100644 index 0000000..8e792a5 --- /dev/null +++ b/next/logger/logger.go @@ -0,0 +1,78 @@ +package logger + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "path" + "runtime" +) + +type logger struct { + logger *zap.Logger + level *zap.AtomicLevel + sugar *zap.SugaredLogger + writers []zapcore.WriteSyncer + stderr zapcore.Core // fixed stderr output + prefix string // custom output prefix + path string // project absolute path + verbose bool // show goroutine id and caller line +} + +var logHandle *logger // singleton logger handle + +// logConfig generates log config for XProxy. +func logConfig(colored bool) zapcore.EncoderConfig { + config := zapcore.EncoderConfig{ + ConsoleSeparator: " ", + MessageKey: "msg", + LevelKey: "level", + TimeKey: "time", + CallerKey: "caller", + EncodeTime: timeEncoder, + EncodeLevel: levelEncoder, + EncodeCaller: callerEncoder, + } + if colored { + config.EncodeTime = timeColoredEncoder + config.EncodeLevel = levelColoredEncoder + config.EncodeCaller = callerColoredEncoder + } + return config +} + +func init() { + zapLevel := zap.NewAtomicLevelAt(InfoLevel) // using info level in default + zapCore := zapcore.NewCore( + zapcore.NewConsoleEncoder(logConfig(true)), // colorful output + zapcore.Lock(os.Stderr), + zapLevel, + ) + zapLogger := zap.New(zapCore, zap.AddCaller(), zap.AddCallerSkip(1)) + _, src, _, _ := runtime.Caller(0) // absolute path of current code + logHandle = &logger{ + logger: zapLogger, + level: &zapLevel, + stderr: zapCore, + sugar: zapLogger.Sugar(), + writers: []zapcore.WriteSyncer{}, + path: path.Join(path.Dir(src), "../"), + prefix: "[XProxy]", + verbose: false, + } +} + +// addWrites adds more plain log writers. +func addWrites(writers ...zapcore.WriteSyncer) { + logHandle.writers = append(logHandle.writers, writers...) + plainCore := zapcore.NewCore( + zapcore.NewConsoleEncoder(logConfig(false)), // without colored + zap.CombineWriteSyncers(logHandle.writers...), + logHandle.level, + ) + logHandle.logger = zap.New( + zapcore.NewTee(logHandle.stderr, plainCore), + zap.AddCaller(), zap.AddCallerSkip(1), + ) + logHandle.sugar = logHandle.logger.Sugar() +}