diff --git a/next/assets/assets.go b/assets/assets.go similarity index 99% rename from next/assets/assets.go rename to assets/assets.go index 1b71b3d..8619271 100644 --- a/next/assets/assets.go +++ b/assets/assets.go @@ -1,7 +1,7 @@ package assets import ( - "XProxy/next/logger" + "XProxy/logger" "github.com/robfig/cron" urlpkg "net/url" "os" diff --git a/next/assets/extract.go b/assets/extract.go similarity index 99% rename from next/assets/extract.go rename to assets/extract.go index c00f804..8c83400 100644 --- a/next/assets/extract.go +++ b/assets/extract.go @@ -1,7 +1,7 @@ package assets import ( - "XProxy/next/logger" + "XProxy/logger" "bytes" "compress/bzip2" "github.com/gabriel-vasile/mimetype" diff --git a/next/assets/extract_test.go b/assets/extract_test.go similarity index 100% rename from next/assets/extract_test.go rename to assets/extract_test.go diff --git a/next/assets/remote.go b/assets/remote.go similarity index 99% rename from next/assets/remote.go rename to assets/remote.go index e648621..b57e9c2 100644 --- a/next/assets/remote.go +++ b/assets/remote.go @@ -1,7 +1,7 @@ package assets import ( - "XProxy/next/logger" + "XProxy/logger" "bytes" "errors" "github.com/andybalholm/brotli" diff --git a/next/assets/update.go b/assets/update.go similarity index 99% rename from next/assets/update.go rename to assets/update.go index b09aa64..799e60b 100644 --- a/next/assets/update.go +++ b/assets/update.go @@ -1,7 +1,7 @@ package assets import ( - "XProxy/next/logger" + "XProxy/logger" urlpkg "net/url" "os" "path" diff --git a/go.mod b/go.mod index b54f409..8322aac 100644 --- a/go.mod +++ b/go.mod @@ -4,27 +4,29 @@ go 1.21.0 require ( github.com/BurntSushi/toml v1.3.2 - github.com/andybalholm/brotli v1.0.5 - github.com/gabriel-vasile/mimetype v1.4.2 + github.com/andybalholm/brotli v1.1.0 + github.com/avast/retry-go v3.0.0+incompatible + github.com/dsnet/compress v0.0.1 + github.com/gabriel-vasile/mimetype v1.4.3 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/klauspost/compress v1.17.6 + github.com/magiconair/properties v1.8.7 + github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 github.com/robfig/cron v1.2.0 github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.4 github.com/ulikunitz/xz v0.5.11 - go.uber.org/zap v1.25.0 + go.uber.org/zap v1.26.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dsnet/compress v0.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.10.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index 6bcc01a..6b01e4f 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,29 @@ 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/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= -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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 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.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67 h1:jik8PHtAIsPlCRJjJzl4udgEf7hawInF9texMeO2jrU= +github.com/petermattis/goid v0.0.0-20231207134359-e60b3f734c67/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= @@ -46,13 +48,13 @@ 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.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.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/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.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/logger/encoder.go similarity index 63% rename from next/logger/encoder.go rename to logger/encoder.go index b9eda34..a190ca5 100644 --- a/next/logger/encoder.go +++ b/logger/encoder.go @@ -1,33 +1,25 @@ package logger import ( - "bytes" - "fmt" "github.com/gookit/color" + "github.com/petermattis/goid" "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 +// / getGid return goroutine id with string. +func getGid() string { + return strconv.FormatInt(goid.Get(), 10) } // getCaller calculate relative source path of caller. func getCaller(ec zapcore.EntryCaller, verbose bool) string { - file, err := filepath.Rel(logHandle.path, ec.File) + file, err := filepath.Rel(project, ec.File) if err != nil { - return "undefined" + return "unknown" } if verbose { return file + ":" + strconv.Itoa(ec.Line) @@ -44,34 +36,28 @@ func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { // 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")), - )) + enc.AppendString(color.Cyan.Render(logger.prefix)) + enc.AppendString(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 { + if logger.verbose { + enc.AppendString("[" + getGid() + "]") + enc.AppendString("[" + getCaller(ec, true) + "]") + } else { 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. +// callerColoredEncoder formats caller in square brackets with magenta color. func callerColoredEncoder(ec zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { - if !logHandle.verbose { + if logger.verbose { + enc.AppendString(color.Blue.Render("[" + getGid() + "]")) + enc.AppendString(color.Magenta.Render("[" + getCaller(ec, true) + "]")) + } else { 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. @@ -79,8 +65,8 @@ func levelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString("[" + level.CapitalString() + "]") } -// levelColoredEncoder formats log level using square brackets -// and uses different colors. +// levelColoredEncoder formats log level using square brackets and uses +// different colors. func levelColoredEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { levelStr := "[" + level.CapitalString() + "]" switch level { diff --git a/logger/encoder_test.go b/logger/encoder_test.go new file mode 100644 index 0000000..23cd8f1 --- /dev/null +++ b/logger/encoder_test.go @@ -0,0 +1,99 @@ +package logger + +import ( + "XProxy/mocks" + "fmt" + "github.com/magiconair/properties/assert" + "github.com/petermattis/goid" + "github.com/stretchr/testify/mock" + "go.uber.org/zap/zapcore" + "runtime" + "strings" + "testing" + "time" +) + +var testTime = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) + +// zapCaller build fake caller with the absolute path of current code. +func zapCaller() zapcore.EntryCaller { + _, srcPath, _, _ := runtime.Caller(0) + return zapcore.EntryCaller{ + File: srcPath, + Line: 0, + } +} + +// encoderTest is a helper function to test buffer output of mock encoder. +func encoderTest(t *testing.T, exec func(*mocks.PrimitiveArrayEncoder), expect string) { + encoder := mocks.NewPrimitiveArrayEncoder(t) + encoder.On("AppendInt64", mock.Anything).Maybe() // only enroll used method + encoder.On("AppendString", mock.Anything).Maybe() + + exec(encoder) + var values []string + for _, call := range encoder.Calls { + values = append(values, fmt.Sprintf("%v", call.Arguments.Get(0))) + } + assert.Equal(t, strings.Join(values, " "), expect) +} + +func Test_getCaller(t *testing.T) { + caller := zapCaller() + caller.File = "Invalid Path" + assert.Equal(t, getCaller(caller, true), "unknown") + assert.Equal(t, getCaller(caller, false), "unknown") + + assert.Equal(t, getCaller(zapCaller(), false), "logger/encoder_test") + assert.Equal(t, getCaller(zapCaller(), true), "logger/encoder_test.go:0") +} + +func Test_timeEncoder(t *testing.T) { + encoderTest(t, func(encoder *mocks.PrimitiveArrayEncoder) { + timeEncoder(testTime, encoder) + }, "2000-01-01 00:00:00.000") + + encoderTest(t, func(encoder *mocks.PrimitiveArrayEncoder) { + timeColoredEncoder(testTime, encoder) + }, "\x1b[36m"+logger.prefix+"\x1b[0m \x1b[90m2000-01-01 00:00:00.000\x1b[0m") +} + +func Test_callerEncoder(t *testing.T) { + verboseVal := logger.verbose + callerTest := func(entry func(zapcore.EntryCaller, zapcore.PrimitiveArrayEncoder), expect string) { + encoderTest(t, func(encoder *mocks.PrimitiveArrayEncoder) { + entry(zapCaller(), encoder) + }, expect) + } + + logger.verbose = false + callerTest(callerEncoder, "[logger/encoder_test]") + callerTest(callerColoredEncoder, "\x1b[35m[logger/encoder_test]\x1b[0m") + + logger.verbose = true + gid := fmt.Sprintf("[%d]", goid.Get()) + callerTest(callerEncoder, gid+" [logger/encoder_test.go:0]") + callerTest(callerColoredEncoder, "\x1b[34m"+gid+"\x1b[0m \x1b[35m[logger/encoder_test.go:0]\x1b[0m") + + logger.verbose = verboseVal +} + +func Test_levelEncoder(t *testing.T) { + levelTest := func(entry func(zapcore.Level, zapcore.PrimitiveArrayEncoder), level zapcore.Level, expect string) { + encoderTest(t, func(encoder *mocks.PrimitiveArrayEncoder) { + entry(level, encoder) + }, expect) + } + + levelTest(levelEncoder, zapcore.DebugLevel, "[DEBUG]") + levelTest(levelEncoder, zapcore.InfoLevel, "[INFO]") + levelTest(levelEncoder, zapcore.WarnLevel, "[WARN]") + levelTest(levelEncoder, zapcore.ErrorLevel, "[ERROR]") + levelTest(levelEncoder, zapcore.PanicLevel, "[PANIC]") + + levelTest(levelColoredEncoder, zapcore.DebugLevel, "\x1b[39m[DEBUG]\x1b[0m") + levelTest(levelColoredEncoder, zapcore.InfoLevel, "\x1b[32m[INFO]\x1b[0m") + levelTest(levelColoredEncoder, zapcore.WarnLevel, "\x1b[33m[WARN]\x1b[0m") + levelTest(levelColoredEncoder, zapcore.ErrorLevel, "\x1b[31m[ERROR]\x1b[0m") + levelTest(levelColoredEncoder, zapcore.PanicLevel, "\x1b[91m[PANIC]\x1b[0m") +} diff --git a/logger/interface.go b/logger/interface.go new file mode 100644 index 0000000..6350292 --- /dev/null +++ b/logger/interface.go @@ -0,0 +1,65 @@ +package logger + +import ( + "go.uber.org/zap/zapcore" + "io" +) + +type Level = zapcore.Level + +const ( + DebugLevel = zapcore.DebugLevel + InfoLevel = zapcore.InfoLevel + WarnLevel = zapcore.WarnLevel + ErrorLevel = zapcore.ErrorLevel + PanicLevel = zapcore.PanicLevel +) + +func Debugf(template string, args ...interface{}) { + logger.entry.Debugf(template, args...) +} + +func Infof(template string, args ...interface{}) { + logger.entry.Infof(template, args...) +} + +func Warnf(template string, args ...interface{}) { + logger.entry.Warnf(template, args...) +} + +func Errorf(template string, args ...interface{}) { + logger.entry.Errorf(template, args...) +} + +func Panicf(template string, args ...interface{}) { + logger.entry.Panicf(template, args...) +} + +// GetLevel return the current logger level. +func GetLevel() Level { + return logger.level.Level() +} + +// SetLevel configure logger output level. Note that debug level will output +// more information and reduce performance. +func SetLevel(level Level) { + logger.level.SetLevel(level) + if level == DebugLevel { + logger.verbose = true + } else { + logger.verbose = false + } +} + +// AddWriters add more writers to target log channel. +func AddWriters(colored bool, writers ...io.Writer) { + var syncWriters []zapcore.WriteSyncer + for _, writer := range writers { + syncWriters = append(syncWriters, zapcore.AddSync(writer)) + } + if !colored { + logger.addPlainWrites(syncWriters...) + } else { + logger.addColoredWrites(syncWriters...) + } +} diff --git a/logger/interface_test.go b/logger/interface_test.go new file mode 100644 index 0000000..c9b954c --- /dev/null +++ b/logger/interface_test.go @@ -0,0 +1,101 @@ +package logger + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "regexp" + "strings" + "testing" +) + +var Levels = []Level{DebugLevel, InfoLevel, WarnLevel, ErrorLevel, PanicLevel} + +func Test_level(t *testing.T) { + assert.Equal(t, DebugLevel, zap.DebugLevel) + assert.Equal(t, InfoLevel, zap.InfoLevel) + assert.Equal(t, WarnLevel, zap.WarnLevel) + assert.Equal(t, ErrorLevel, zap.ErrorLevel) + assert.Equal(t, PanicLevel, zap.PanicLevel) + + for _, level := range Levels { + SetLevel(level) + assert.Equal(t, GetLevel(), level) + assert.Equal(t, logger.verbose, level == DebugLevel) // verbose only for DEBUG level + } + SetLevel(InfoLevel) // revert to INFO level +} + +func Test_logger(t *testing.T) { + var plainBuf, plainTeeBuf bytes.Buffer + var coloredBuf, coloredTeeBuf bytes.Buffer + logger.plain.writers = []zapcore.WriteSyncer{} // clear slice + logger.colored.writers = []zapcore.WriteSyncer{} + AddWriters(false, &plainBuf, &plainTeeBuf) + AddWriters(true, &coloredBuf, &coloredTeeBuf) + logger.update() // apply test writers + + printLogs := func(usingLevel Level) { + SetLevel(usingLevel) + Debugf("Here is %s level", "DEBUG") + Infof("Here is %s level", "INFO") + Warnf("Here is %s level", "WARN") + Errorf("Here is %s level", "ERROR") + assert.Panics(t, func() { + Panicf("Here is %s level", "PANIC") + }) + } + printLogs(DebugLevel) // output into buffer + printLogs(InfoLevel) + printLogs(WarnLevel) + printLogs(ErrorLevel) + printLogs(PanicLevel) + + assertLine := func(log string, level Level, colored bool, verbose bool) { + regex := `^([\d.:\- ]+) \[(\S+)] (\[\d+] )?\[(\S+)] Here is (\S+) level$` + if colored { + regex = `^\x1b\[36m\[XProxy]\x1b\[0m \x1b\[90m([\d.:\- ]+)\x1b\[0m ` + + `\x1b\[\d\dm\[(\S+)]\x1b\[0m (\x1b\[34m\[\d+]\x1b\[0m )?` + + `\x1b\[35m\[(\S+)]\x1b\[0m Here is (\S+) level$` + } + matches := regexp.MustCompile(regex).FindStringSubmatch(log) + timeRegex := regexp.MustCompile(`^\d{4}(-\d\d){2} \d{2}(:\d\d){2}\.\d{3}$`) + + assert.NotEmpty(t, matches) // valid log line + assert.Regexp(t, timeRegex, matches[1]) // valid time format + assert.Equal(t, level.CapitalString(), matches[2]) // valid level string + assert.Equal(t, level.CapitalString(), matches[5]) + if !verbose { + assert.Equal(t, "logger/interface_test", matches[4]) + } else { + assert.Regexp(t, regexp.MustCompile(`^logger/interface_test.go:\d+$`), matches[4]) + } + } + + assertLogs := func(buffer string, colored bool) { + var line string + logs := strings.Split(buffer, "\n") + for _, limit := range Levels { + for _, level := range Levels { + if level >= limit { + line, logs = logs[0], logs[1:] + assertLine(line, level, colored, limit == DebugLevel) + } + } + } + assert.Equal(t, len(logs), 1) + assert.Equal(t, logs[0], "") // last line is empty + } + + assertLogs(plainBuf.String(), false) + assertLogs(coloredBuf.String(), true) + assert.Equal(t, plainBuf.String(), plainTeeBuf.String()) + assert.Equal(t, coloredBuf.String(), coloredTeeBuf.String()) + + logger.plain.writers = []zapcore.WriteSyncer{} // revert logger configure + logger.colored.writers = []zapcore.WriteSyncer{os.Stdout} + logger.level.SetLevel(InfoLevel) + logger.update() +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..0c7ebc1 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,95 @@ +package logger + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "path" + "runtime" +) + +var project string // project absolute path +var logger *logCore // singleton logger handle + +// logChannel handle multiple writers with unified format. +type logChannel struct { + encoder zapcore.Encoder + writers []zapcore.WriteSyncer +} + +// logCore manage log level, channels and other interfaces. +type logCore struct { + prefix string // custom output prefix + verbose bool // show verbose information + + plain logChannel // log channel with plain text + colored logChannel // log channel with colored text + + level *zap.AtomicLevel // zap log level pointer + entry *zap.SugaredLogger // zap sugared logger entry +} + +func init() { + _, src, _, _ := runtime.Caller(0) // absolute path of current code + project = path.Join(path.Dir(src), "../") + + zapLevel := zap.NewAtomicLevelAt(InfoLevel) // using info level in default + logger = &logCore{ + verbose: false, + level: &zapLevel, + prefix: "[XProxy]", + plain: buildChannel(false), + colored: buildChannel(true), + } + logger.addColoredWrites(os.Stderr) // output into stderr in default +} + +// buildChannel generate logChannel with `colored` option. +func buildChannel(colored bool) logChannel { + config := zapcore.EncoderConfig{ + ConsoleSeparator: " ", + MessageKey: "msg", + LevelKey: "level", + TimeKey: "time", + CallerKey: "caller", + EncodeTime: timeEncoder, + EncodeLevel: levelEncoder, + EncodeCaller: callerEncoder, + } + if colored { // using colored version + config.EncodeTime = timeColoredEncoder + config.EncodeLevel = levelColoredEncoder + config.EncodeCaller = callerColoredEncoder + } + return logChannel{ + encoder: zapcore.NewConsoleEncoder(config), + writers: []zapcore.WriteSyncer{}, // without any writer + } +} + +// update refreshes the binding of the log core to the writers. +func (handle *logCore) update() { + buildCore := func(channel *logChannel) zapcore.Core { // build zap core from logChannel + return zapcore.NewCore( + channel.encoder, + zap.CombineWriteSyncers(channel.writers...), + handle.level, + ) + } + handle.entry = zap.New( + zapcore.NewTee(buildCore(&handle.plain), buildCore(&handle.colored)), + zap.AddCaller(), zap.AddCallerSkip(1), + ).Named("xproxy").Sugar() +} + +// addPlainWrites adds plain text writers to the logCore. +func (handle *logCore) addPlainWrites(writers ...zapcore.WriteSyncer) { + handle.plain.writers = append(handle.plain.writers, writers...) + handle.update() +} + +// addColoredWrites adds colored text writers to the logCore. +func (handle *logCore) addColoredWrites(writers ...zapcore.WriteSyncer) { + handle.colored.writers = append(handle.colored.writers, writers...) + handle.update() +} diff --git a/logger/logger_test.go b/logger/logger_test.go new file mode 100644 index 0000000..08ef07e --- /dev/null +++ b/logger/logger_test.go @@ -0,0 +1,47 @@ +package logger + +import ( + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zapcore" + "os" + "path" + "testing" +) + +func Test_init(t *testing.T) { + pwd := path.Dir(zapCaller().File) + assert.Equal(t, path.Join(pwd, "../"), project) + + assert.NotNil(t, logger) + assert.NotNil(t, logger.entry) + assert.NotNil(t, logger.level) + + assert.Equal(t, logger.verbose, false) + assert.Equal(t, logger.prefix, "[XProxy]") + assert.Equal(t, logger.level.Level(), InfoLevel) + + assert.Equal(t, len(logger.plain.writers), 0) + assert.Equal(t, len(logger.colored.writers), 1) + + for _, level := range []zapcore.Level{DebugLevel, InfoLevel, WarnLevel, ErrorLevel, PanicLevel} { + logger.level.SetLevel(level) + assert.Equal(t, logger.entry.Level(), level) + } + logger.level.SetLevel(InfoLevel) // revert to INFO level +} + +func Test_addWrites(t *testing.T) { + core := new(logCore) + assert.Equal(t, len(core.plain.writers), 0) + assert.Equal(t, len(core.colored.writers), 0) + + core.entry = nil + core.addPlainWrites(os.Stdout, os.Stdout, os.Stdout) + assert.NotNil(t, core.entry) + assert.Equal(t, len(core.plain.writers), 3) + + core.entry = nil + core.addColoredWrites(os.Stderr, os.Stderr, os.Stderr) + assert.NotNil(t, core.entry) + assert.Equal(t, len(core.plain.writers), 3) +} diff --git a/next/main.go b/main.go similarity index 96% rename from next/main.go rename to main.go index c320568..3632f7e 100644 --- a/next/main.go +++ b/main.go @@ -1,6 +1,6 @@ package main -import "XProxy/next/assets" +import "XProxy/assets" func main() { remoteAssets := map[string]string{ diff --git a/mocks/PrimitiveArrayEncoder.go b/mocks/PrimitiveArrayEncoder.go new file mode 100644 index 0000000..d78572e --- /dev/null +++ b/mocks/PrimitiveArrayEncoder.go @@ -0,0 +1,114 @@ +// Code generated by mockery v2.40.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// PrimitiveArrayEncoder is an autogenerated mock type for the PrimitiveArrayEncoder type +type PrimitiveArrayEncoder struct { + mock.Mock +} + +// AppendBool provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendBool(_a0 bool) { + _m.Called(_a0) +} + +// AppendByteString provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendByteString(_a0 []byte) { + _m.Called(_a0) +} + +// AppendComplex128 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendComplex128(_a0 complex128) { + _m.Called(_a0) +} + +// AppendComplex64 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendComplex64(_a0 complex64) { + _m.Called(_a0) +} + +// AppendFloat32 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendFloat32(_a0 float32) { + _m.Called(_a0) +} + +// AppendFloat64 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendFloat64(_a0 float64) { + _m.Called(_a0) +} + +// AppendInt provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendInt(_a0 int) { + _m.Called(_a0) +} + +// AppendInt16 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendInt16(_a0 int16) { + _m.Called(_a0) +} + +// AppendInt32 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendInt32(_a0 int32) { + _m.Called(_a0) +} + +// AppendInt64 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendInt64(_a0 int64) { + _m.Called(_a0) +} + +// AppendInt8 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendInt8(_a0 int8) { + _m.Called(_a0) +} + +// AppendString provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendString(_a0 string) { + _m.Called(_a0) +} + +// AppendUint provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendUint(_a0 uint) { + _m.Called(_a0) +} + +// AppendUint16 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendUint16(_a0 uint16) { + _m.Called(_a0) +} + +// AppendUint32 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendUint32(_a0 uint32) { + _m.Called(_a0) +} + +// AppendUint64 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendUint64(_a0 uint64) { + _m.Called(_a0) +} + +// AppendUint8 provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendUint8(_a0 uint8) { + _m.Called(_a0) +} + +// AppendUintptr provides a mock function with given fields: _a0 +func (_m *PrimitiveArrayEncoder) AppendUintptr(_a0 uintptr) { + _m.Called(_a0) +} + +// NewPrimitiveArrayEncoder creates a new instance of PrimitiveArrayEncoder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPrimitiveArrayEncoder(t interface { + mock.TestingT + Cleanup(func()) +}) *PrimitiveArrayEncoder { + mock := &PrimitiveArrayEncoder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/next/logger/interface.go b/next/logger/interface.go deleted file mode 100644 index f58b811..0000000 --- a/next/logger/interface.go +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 8e792a5..0000000 --- a/next/logger/logger.go +++ /dev/null @@ -1,78 +0,0 @@ -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() -}