├── go.sum ├── .gitignore ├── go.mod ├── handler_test.go ├── .github └── workflows │ └── go.yml ├── LICENSE.txt ├── handler.go ├── utils_test.go ├── log ├── log.go └── log_test.go ├── README.md ├── utils.go ├── log_test.go ├── logger.go ├── formatter.go ├── formatter_test.go ├── writer_test.go └── writer.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keakon/golog 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package golog 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestHandle(t *testing.T) { 8 | h := NewHandler(InfoLevel, DefaultFormatter) 9 | r := &Record{tm: now()} 10 | if h.Handle(r) { 11 | t.Error("info handler handled debug record") 12 | } 13 | 14 | r.level = InfoLevel 15 | if !h.Handle(r) { 16 | t.Error("info handler ignored info record") 17 | } 18 | 19 | r.level = ErrorLevel 20 | if !h.Handle(r) { 21 | t.Error("error handler ignored info record") 22 | } 23 | } 24 | 25 | func TestCloseHandler(t *testing.T) { 26 | h := NewHandler(InfoLevel, DefaultFormatter) 27 | h.Close() 28 | h.Close() 29 | 30 | w := NewDiscardWriter() 31 | h.AddWriter(w) 32 | h.Close() 33 | if len(h.writers) > 0 { 34 | t.Error("closed handler is not empty") 35 | } 36 | if w.Writer != nil { 37 | t.Error("close handler left its writer opened") 38 | } 39 | h.Close() 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '*.md' 7 | pull_request: 8 | paths-ignore: 9 | - '*.md' 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | go: [ '1.19', '1.20' ] 17 | os: [ ubuntu-latest, macos-latest, windows-latest ] 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v3 26 | with: 27 | go-version: ${{ matrix.go }} 28 | 29 | - name: Race test 30 | run: go test -race 31 | 32 | - name: Test with coverage 33 | if: ${{ matrix.go == '1.20' && matrix.os == 'ubuntu-latest' }} 34 | run: go test -covermode=atomic -coverprofile=coverage -benchmem -bench=. . ./log 35 | 36 | - name: Upload code coverage report 37 | if: ${{ matrix.go == '1.20' && matrix.os == 'ubuntu-latest' }} 38 | uses: codecov/codecov-action@v3 39 | with: 40 | files: coverage 41 | flags: unittests 42 | verbose: true 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 keakon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package golog 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // A Handler is a leveled log handler with a formatter and several writers. 9 | type Handler struct { 10 | writers []io.WriteCloser 11 | formatter *Formatter 12 | level Level 13 | isInternal bool 14 | } 15 | 16 | // NewHandler creates a new Handler of the given level with the formatter. 17 | // Records with the lower level than the handler will be ignored. 18 | func NewHandler(level Level, formatter *Formatter) *Handler { 19 | return &Handler{ 20 | level: level, 21 | formatter: formatter, 22 | } 23 | } 24 | 25 | // AddWriter adds a writer to the Handler. 26 | // The Write() method of the writer should be thread-safe. 27 | func (h *Handler) AddWriter(w io.WriteCloser) { 28 | h.writers = append(h.writers, w) 29 | } 30 | 31 | // Handle formats a record using its formatter, then writes the formatted result to all of its writers. 32 | // Returns true if it can handle the record. 33 | // The errors during writing will be logged by the internalLogger. 34 | // It's not thread-safe, concurrent record may be written in a random order through different writers. 35 | // But two records won't be mixed in a single line. 36 | func (h *Handler) Handle(r *Record) bool { 37 | if r.level >= h.level { 38 | buf := bufPool.Get().(*bytes.Buffer) 39 | buf.Reset() 40 | h.formatter.Format(r, buf) 41 | content := buf.Bytes() 42 | for _, w := range h.writers { 43 | _, err := w.Write(content) 44 | if err != nil && !h.isInternal { 45 | logError(err) 46 | } 47 | } 48 | bufPool.Put(buf) 49 | return true 50 | } 51 | return false 52 | } 53 | 54 | // Close closes all its writers. 55 | // It's safe to call this method more than once, 56 | // but it's unsafe to call its writers' Close() more than once. 57 | func (h *Handler) Close() { 58 | for _, w := range h.writers { 59 | err := w.Close() 60 | if err != nil { 61 | logError(err) 62 | } 63 | } 64 | h.writers = nil 65 | } 66 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package golog 2 | 3 | import "testing" 4 | 5 | func TestUint2Bytes(t *testing.T) { 6 | bs := string(uint2Bytes(0, 2)) 7 | if bs != "00" { 8 | t.Errorf("result is " + bs) 9 | } 10 | 11 | bs = string(uint2Bytes(59, 2)) 12 | if bs != "59" { 13 | t.Errorf("result is " + bs) 14 | } 15 | 16 | bs = string(uint2Bytes(1970, 4)) 17 | if bs != "1970" { 18 | t.Errorf("result is " + bs) 19 | } 20 | 21 | bs = string(uint2Bytes(2038, 4)) 22 | if bs != "2038" { 23 | t.Errorf("result is " + bs) 24 | } 25 | } 26 | 27 | func TestUint2Bytes2(t *testing.T) { 28 | bs := string(uint2Bytes2(0)) 29 | if bs != "00" { 30 | t.Errorf("result is " + bs) 31 | } 32 | 33 | bs = string(uint2Bytes2(59)) 34 | if bs != "59" { 35 | t.Errorf("result is " + bs) 36 | } 37 | } 38 | 39 | func TestUint2Bytes4(t *testing.T) { 40 | bs := string(uint2Bytes4(1970)) 41 | if bs != "1970" { 42 | t.Errorf("result is " + bs) 43 | } 44 | 45 | bs = string(uint2Bytes4(2038)) 46 | if bs != "2038" { 47 | t.Errorf("result is " + bs) 48 | } 49 | } 50 | 51 | func TestUint2DynamicBytes(t *testing.T) { 52 | bs := string(uint2DynamicBytes(0)) 53 | if bs != "0" { 54 | t.Errorf("result is " + bs) 55 | } 56 | 57 | bs = string(uint2DynamicBytes(59)) 58 | if bs != "59" { 59 | t.Errorf("result is " + bs) 60 | } 61 | 62 | bs = string(uint2DynamicBytes(999)) 63 | if bs != "999" { 64 | t.Errorf("result is " + bs) 65 | } 66 | 67 | bs = string(uint2DynamicBytes(1000)) 68 | if bs != "1000" { 69 | t.Errorf("result is " + bs) 70 | } 71 | 72 | bs = string(uint2DynamicBytes(1970)) 73 | if bs != "1970" { 74 | t.Errorf("result is " + bs) 75 | } 76 | 77 | bs = string(uint2DynamicBytes(2038)) 78 | if bs != "2038" { 79 | t.Errorf("result is " + bs) 80 | } 81 | 82 | bs = string(uint2DynamicBytes(12345)) 83 | if bs != "12345" { 84 | t.Errorf("result is " + bs) 85 | } 86 | 87 | bs = string(uint2DynamicBytes(123456)) 88 | if bs != "123456" { 89 | t.Errorf("result is " + bs) 90 | } 91 | 92 | bs = string(uint2DynamicBytes(1234567)) 93 | if bs != "1234567" { 94 | t.Errorf("result is " + bs) 95 | } 96 | 97 | bs = string(uint2DynamicBytes(12345678)) 98 | if bs != "12345678" { 99 | t.Errorf("result is " + bs) 100 | } 101 | 102 | bs = string(uint2DynamicBytes(123456789)) 103 | if bs != "123456789" { 104 | t.Errorf("result is " + bs) 105 | } 106 | 107 | bs = string(uint2DynamicBytes(1234567890)) 108 | if bs != "1234567890" { 109 | t.Errorf("result is " + bs) 110 | } 111 | 112 | bs = string(uint2DynamicBytes(2<<31 - 1)) 113 | if bs != "4294967295" { 114 | t.Errorf("result is " + bs) 115 | } 116 | } 117 | 118 | func TestFastUint2DynamicBytes(t *testing.T) { 119 | bs := string(fastUint2DynamicBytes(0)) 120 | if bs != "0" { 121 | t.Errorf("result is " + bs) 122 | } 123 | 124 | bs = string(fastUint2DynamicBytes(59)) 125 | if bs != "59" { 126 | t.Errorf("result is " + bs) 127 | } 128 | 129 | bs = string(fastUint2DynamicBytes(999)) 130 | if bs != "999" { 131 | t.Errorf("result is " + bs) 132 | } 133 | 134 | bs = string(fastUint2DynamicBytes(1000)) 135 | if bs != "1000" { 136 | t.Errorf("result is " + bs) 137 | } 138 | 139 | bs = string(fastUint2DynamicBytes(1970)) 140 | if bs != "1970" { 141 | t.Errorf("result is " + bs) 142 | } 143 | 144 | bs = string(fastUint2DynamicBytes(2038)) 145 | if bs != "2038" { 146 | t.Errorf("result is " + bs) 147 | } 148 | 149 | bs = string(fastUint2DynamicBytes(12345)) 150 | if bs != "12345" { 151 | t.Errorf("result is " + bs) 152 | } 153 | 154 | bs = string(fastUint2DynamicBytes(123456)) 155 | if bs != "123456" { 156 | t.Errorf("result is " + bs) 157 | } 158 | 159 | bs = string(fastUint2DynamicBytes(1234567)) 160 | if bs != "1234567" { 161 | t.Errorf("result is " + bs) 162 | } 163 | 164 | bs = string(fastUint2DynamicBytes(12345678)) 165 | if bs != "12345678" { 166 | t.Errorf("result is " + bs) 167 | } 168 | 169 | bs = string(fastUint2DynamicBytes(123456789)) 170 | if bs != "123456789" { 171 | t.Errorf("result is " + bs) 172 | } 173 | 174 | bs = string(fastUint2DynamicBytes(1234567890)) 175 | if bs != "1234567890" { 176 | t.Errorf("result is " + bs) 177 | } 178 | 179 | bs = string(fastUint2DynamicBytes(2<<31 - 1)) 180 | if bs != "4294967295" { 181 | t.Errorf("result is " + bs) 182 | } 183 | } 184 | 185 | func BenchmarkCaller(b *testing.B) { 186 | for i := 0; i < b.N; i++ { 187 | Caller(1) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/keakon/golog" 5 | ) 6 | 7 | var defaultLogger *golog.Logger 8 | 9 | // SetLogFunc set the log function with specified level for the defaultLogger. 10 | // This function should be called before SetDefaultLogger. 11 | func SetLogFunc(f func(args ...interface{}), level golog.Level) { 12 | switch level { 13 | case golog.DebugLevel: 14 | Debug = f 15 | case golog.InfoLevel: 16 | Info = f 17 | case golog.WarnLevel: 18 | Warn = f 19 | case golog.ErrorLevel: 20 | Error = f 21 | case golog.CritLevel: 22 | Crit = f 23 | } 24 | } 25 | 26 | // SetLogfFunc set the logf function with specified level for the defaultLogger. 27 | // This function should be called before SetDefaultLogger. 28 | func SetLogfFunc(f func(msg string, args ...interface{}), level golog.Level) { 29 | switch level { 30 | case golog.DebugLevel: 31 | Debugf = f 32 | case golog.InfoLevel: 33 | Infof = f 34 | case golog.WarnLevel: 35 | Warnf = f 36 | case golog.ErrorLevel: 37 | Errorf = f 38 | case golog.CritLevel: 39 | Critf = f 40 | } 41 | } 42 | 43 | // SetDefaultLogger set the logger as the defaultLogger. 44 | // The logging functions in this package use it as their logger. 45 | // This function should be called before using below functions. 46 | func SetDefaultLogger(l *golog.Logger) { 47 | defaultLogger = l 48 | if l == nil { 49 | return 50 | } 51 | minLevel := l.GetMinLevel() 52 | for level := golog.DebugLevel; level <= golog.CritLevel; level++ { 53 | if level < minLevel { 54 | switch level { 55 | case golog.DebugLevel: 56 | Debug = nop 57 | Debugf = nopf 58 | case golog.InfoLevel: 59 | Info = nop 60 | Infof = nopf 61 | case golog.WarnLevel: 62 | Warn = nop 63 | Warnf = nopf 64 | case golog.ErrorLevel: 65 | Error = nop 66 | Errorf = nopf 67 | case golog.CritLevel: 68 | Crit = nop 69 | Critf = nopf 70 | } 71 | } else { 72 | switch level { 73 | case golog.DebugLevel: 74 | Debug = logFuncs[level] 75 | Debugf = logfFuncs[level] 76 | case golog.InfoLevel: 77 | Info = logFuncs[level] 78 | Infof = logfFuncs[level] 79 | case golog.WarnLevel: 80 | Warn = logFuncs[level] 81 | Warnf = logfFuncs[level] 82 | case golog.ErrorLevel: 83 | Error = logFuncs[level] 84 | Errorf = logfFuncs[level] 85 | case golog.CritLevel: 86 | Crit = logFuncs[level] 87 | Critf = logfFuncs[level] 88 | } 89 | } 90 | } 91 | } 92 | 93 | // IsEnabledFor returns whether the default logger is enabled for the level. 94 | func IsEnabledFor(level golog.Level) bool { 95 | return defaultLogger != nil && defaultLogger.IsEnabledFor(level) 96 | } 97 | 98 | func nop(args ...interface{}) {} 99 | func nopf(msg string, args ...interface{}) {} 100 | 101 | var ( 102 | // Debug logs a _debug level message. It uses fmt.Fprint() to format args. 103 | Debug = nop 104 | // Info logs a _info level message. It uses fmt.Fprint() to format args. 105 | Info = nop 106 | // Warn logs a _warning level message. It uses fmt.Fprint() to format args. 107 | Warn = nop 108 | // Error logs an _error level message. It uses fmt.Fprint() to format args. 109 | Error = nop 110 | // Crit logs a _critical level message. It uses fmt.Fprint() to format args. 111 | Crit = nop 112 | 113 | // Debugf logs a _debug level message. It uses fmt.Fprintf() to format msg and args. 114 | Debugf = nopf 115 | // Infof logs a _info level message. It uses fmt.Fprintf() to format msg and args. 116 | Infof = nopf 117 | // Warnf logs a _warning level message. It uses fmt.Fprintf() to format msg and args. 118 | Warnf = nopf 119 | // Errorf logs a _error level message. It uses fmt.Fprintf() to format msg and args. 120 | Errorf = nopf 121 | // Critf logs a _critical level message. It uses fmt.Fprintf() to format msg and args. 122 | Critf = nopf 123 | 124 | logFuncs = []func(args ...interface{}){ 125 | func(args ...interface{}) { 126 | file, line := golog.Caller(1) // deeper caller will be more expensive, so don't use init() to initialize those functions 127 | defaultLogger.Log(golog.DebugLevel, file, line, "", args...) 128 | }, 129 | func(args ...interface{}) { 130 | file, line := golog.Caller(1) 131 | defaultLogger.Log(golog.InfoLevel, file, line, "", args...) 132 | }, 133 | func(args ...interface{}) { 134 | file, line := golog.Caller(1) 135 | defaultLogger.Log(golog.WarnLevel, file, line, "", args...) 136 | }, 137 | func(args ...interface{}) { 138 | file, line := golog.Caller(1) 139 | defaultLogger.Log(golog.ErrorLevel, file, line, "", args...) 140 | }, 141 | func(args ...interface{}) { 142 | file, line := golog.Caller(1) 143 | defaultLogger.Log(golog.CritLevel, file, line, "", args...) 144 | }, 145 | } 146 | 147 | logfFuncs = []func(msg string, args ...interface{}){ 148 | func(msg string, args ...interface{}) { 149 | file, line := golog.Caller(1) 150 | defaultLogger.Log(golog.DebugLevel, file, line, msg, args...) 151 | }, 152 | func(msg string, args ...interface{}) { 153 | file, line := golog.Caller(1) 154 | defaultLogger.Log(golog.InfoLevel, file, line, msg, args...) 155 | }, 156 | func(msg string, args ...interface{}) { 157 | file, line := golog.Caller(1) 158 | defaultLogger.Log(golog.WarnLevel, file, line, msg, args...) 159 | }, 160 | func(msg string, args ...interface{}) { 161 | file, line := golog.Caller(1) 162 | defaultLogger.Log(golog.ErrorLevel, file, line, msg, args...) 163 | }, 164 | func(msg string, args ...interface{}) { 165 | file, line := golog.Caller(1) 166 | defaultLogger.Log(golog.CritLevel, file, line, msg, args...) 167 | }, 168 | } 169 | ) 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golog 2 | [![GoDoc](https://pkg.go.dev/badge/github.com/keakon/golog)](https://pkg.go.dev/github.com/keakon/golog) 3 | [![Build Status](https://github.com/keakon/golog/actions/workflows/go.yml/badge.svg)](https://github.com/keakon/golog/actions) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/keakon/golog)](https://goreportcard.com/report/github.com/keakon/golog) 5 | [![codecov](https://codecov.io/gh/keakon/golog/branch/master/graph/badge.svg?token=dmE2GC9in2)](https://codecov.io/gh/keakon/golog) 6 | 7 | ## Features 8 | 9 | 1. Unstructured 10 | 2. Leveled 11 | 3. With caller (file path / name and line number) 12 | 4. Customizable output format 13 | 5. Rotating by size, date or hour 14 | 6. Cross platform, tested on Linux, macOS and Windows 15 | 7. No 3rd party dependance 16 | 8. Fast 17 | 9. Thread safe 18 | 19 | ## Installation 20 | 21 | ``` 22 | go get -u github.com/keakon/golog 23 | ``` 24 | 25 | ## Examples 26 | 27 | ### Logging to console 28 | 29 | ```go 30 | package main 31 | 32 | import ( 33 | "github.com/keakon/golog" 34 | "github.com/keakon/golog/log" 35 | ) 36 | 37 | func main() { 38 | l := golog.NewStdoutLogger() 39 | defer l.Close() 40 | 41 | l.Infof("hello %d", 1) 42 | 43 | log.SetDefaultLogger(l) 44 | test() 45 | } 46 | 47 | func test() { 48 | log.Infof("hello %d", 2) 49 | } 50 | ``` 51 | 52 | ### Logging to file 53 | 54 | ```go 55 | func main() { 56 | w, _ := golog.NewBufferedFileWriter("test.log") 57 | l := golog.NewLoggerWithWriter(w) 58 | defer l.Close() 59 | 60 | l.Infof("hello world") 61 | } 62 | ``` 63 | 64 | ### Rotating 65 | 66 | ```go 67 | func main() { 68 | w, _ := golog.NewTimedRotatingFileWriter("test", golog.RotateByDate, 30) 69 | l := golog.NewLoggerWithWriter(w) 70 | defer l.Close() 71 | 72 | l.Infof("hello world") 73 | } 74 | ``` 75 | 76 | ### Formatting 77 | 78 | ```go 79 | func main() { 80 | w := golog.NewStdoutWriter() 81 | 82 | f := golog.ParseFormat("[%l %D %T %S] %m") 83 | h := golog.NewHandler(golog.InfoLevel, f) 84 | h.AddWriter(w) 85 | 86 | l := golog.NewLogger(golog.InfoLevel) 87 | l.AddHandler(h) 88 | defer l.Close() 89 | 90 | l.Infof("hello world") 91 | } 92 | ``` 93 | 94 | Check [document](https://pkg.go.dev/github.com/keakon/golog#Formatter.Format) for more format directives. 95 | 96 | ### Fast timer 97 | 98 | ```go 99 | func main() { 100 | golog.StartFastTimer() 101 | defer golog.StopFastTimer() 102 | 103 | l := golog.NewStdoutLogger() 104 | defer l.Close() 105 | 106 | l.Infof("hello world") 107 | } 108 | ``` 109 | 110 | The fast timer is about 30% faster than calling time.Time() for each logging record. But it's not thread-safe which may cause some problems (I think those are negligible in most cases): 111 | 1. The timer updates every 1 second, so the logging time can be at most 1 second behind the real time. 112 | 2. Each thread will notice the changes of timer in a few milliseconds, so the concurrent logging messages may get different logging time (less than 2% probability). eg: 113 | ``` 114 | [I 2021-09-13 14:31:25 log_test:206] test 115 | [I 2021-09-13 14:31:24 log_test:206] test 116 | [I 2021-09-13 14:31:25 log_test:206] test 117 | ``` 118 | 3. When the day changing, the logging date and time might belong to different day. eg: 119 | ``` 120 | [I 2021-09-12 23:59:59 log_test:206] test 121 | [I 2021-09-13 23:59:59 log_test:206] test 122 | [I 2021-09-12 00:00:00 log_test:206] test 123 | ``` 124 | 125 | ### ConcurrentFileWriter *(experimental)* 126 | 127 | 128 | ```go 129 | func main() { 130 | w, _ := golog.NewConcurrentFileWriter("test.log") 131 | l := golog.NewLoggerWithWriter(w) 132 | defer l.Close() 133 | 134 | l.Infof("hello world") 135 | } 136 | ``` 137 | 138 | The `ConcurrentFileWriter` is designed for high concurrency applications. 139 | It is about 140% faster than `BufferedFileWriter` at 6C12H by reducing the lock overhead, but a little slower at single thread. 140 | **Note**: The order of logging records from different cpu cores within each 0.1 second is random. 141 | 142 | ## Benchmarks 143 | 144 | ### Platform 145 | 146 | go1.19.2 darwin/amd64 147 | cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 148 | 149 | ### Result 150 | 151 | | Name | Time/op | Time (x) | Alloc/op | allocs/op | 152 | | :--- | :---: | :---: | :---: | :---: | 153 | | DiscardLogger | 483ns ± 1% | 1.00 | 0B | 0 | 154 | | DiscardLoggerParallel | 89.0ns ± 6% | 1.00 | 0B | 0 | 155 | | DiscardLoggerWithoutTimer | 691ns ± 7% | 1.43 | 0B | 0 | 156 | | DiscardLoggerWithoutTimerParallel | 129ns ± 5% | 1.45 | 0B | 0 | 157 | | NopLog | 1.5ns ± 1% | 0.003 | 0B | 0 | 158 | | NopLogParallel | 0.22ns ± 3% | 0.002 | 0B | 0 | 159 | | MultiLevels | 2.77µs ± 7% | 5.73 | 0B | 0 | 160 | | MultiLevelsParallel | 532ns ± 15% | 5.98 | 0B | 0 | 161 | | BufferedFileLogger | 588ns ± 2% | 1.22 | 0B | 0 | 162 | | BufferedFileLoggerParallel | 241ns ± 1% | 2.71 | 0B | 0 | 163 | | ConcurrentFileLogger | 593ns ± 1% | 1.23 | 0B | 0 | 164 | | ConcurrentFileLoggerParallel | 101ns ± 2% | 1.13 | 0B | 0 | 165 | | | | | | | 166 | | DiscardZerolog | 2.24µs ± 1% | 4.64 | 280B | 3 | 167 | | DiscardZerologParallel | 408ns ± 10% | 4.58 | 280B | 3 | 168 | | DiscardZap | 2.13µs ± 0% | 4.41 | 272B | 5 | 169 | | DiscardZapParallel | 465ns ± 5% | 5.22 | 274B | 5 | 170 | 171 | * DiscardLogger: writes logs to `ioutil.Discard` 172 | * DiscardLoggerWithoutTimer: the same as above but without fast timer 173 | * NopLog: skips logs with lower level than the logger or handler 174 | * MultiLevels: writes 5 levels of logs to 5 levels handlers of a warning level logger 175 | * BufferedFileLogger: writes logs to a disk file 176 | * ConcurrentFileLogger: writes logs to a disk file with `ConcurrentFileWriter` 177 | * DiscardZerolog: writes logs to `ioutil.Discard` with [zerolog](https://github.com/rs/zerolog) 178 | * DiscardZap: writes logs to `ioutil.Discard` with [zap](https://github.com/uber-go/zap) using `zap.NewProductionEncoderConfig()` 179 | 180 | All the logs include 4 parts: level, time, caller and message. This is an example output of the benchmarks: 181 | 182 | ``` 183 | [I 2018-11-20 17:05:37 log_test:118] test 184 | ``` 185 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package golog 2 | 3 | import ( 4 | "bytes" 5 | "runtime" 6 | "sync" 7 | "time" 8 | _ "unsafe" 9 | ) 10 | 11 | const ( 12 | recordBufSize = 128 13 | dateTimeBufSize = 10 // length of date string 14 | ) 15 | 16 | var ( 17 | recordPool = sync.Pool{ 18 | New: func() interface{} { 19 | return &Record{} 20 | }, 21 | } 22 | 23 | bufPool = sync.Pool{ 24 | New: func() interface{} { 25 | return bytes.NewBuffer(make([]byte, 0, recordBufSize)) 26 | }, 27 | } 28 | 29 | uintBytes2 [60][]byte // 0 - 59 30 | uintBytes4 [69][]byte // 1970 - 2038 31 | uintBytes [999][]byte // 2 - 1000 32 | 33 | frameCache sync.Map 34 | 35 | now = time.Now 36 | 37 | fastTimer = FastTimer{} 38 | ) 39 | 40 | func init() { 41 | for i := 0; i < 60; i++ { // hour / minute / second is between 0 and 59 42 | uintBytes2[i] = uint2Bytes(i, 2) 43 | } 44 | for i := 0; i < 69; i++ { // year is between 1970 and 2038 45 | uintBytes4[i] = uint2Bytes(1970+i, 4) 46 | } 47 | for i := 0; i < 999; i++ { // source code line number is usually between 2 and 1000 48 | uintBytes[i] = uint2DynamicBytes(i + 2) 49 | } 50 | } 51 | 52 | func uint2Bytes(x, size int) []byte { 53 | // x and size should be uint32 54 | result := make([]byte, size) 55 | for i := 0; i < size; i++ { 56 | r := x % 10 57 | result[size-i-1] = byte(r) + '0' 58 | x /= 10 59 | } 60 | return result 61 | } 62 | 63 | func uint2DynamicBytes(x int) []byte { 64 | // x should be uint32 65 | size := 0 66 | switch { 67 | case x < 10: 68 | return []byte{byte(x) + '0'} 69 | case x < 100: 70 | size = 2 71 | case x < 1000: 72 | size = 3 73 | case x < 10000: 74 | size = 4 75 | case x < 100000: 76 | size = 5 77 | case x < 1000000: 78 | size = 6 79 | case x < 10000000: 80 | size = 7 81 | case x < 100000000: 82 | size = 8 83 | case x < 1000000000: 84 | size = 9 85 | default: 86 | size = 10 87 | } 88 | result := make([]byte, size) 89 | for i := 0; i < size; i++ { 90 | r := x % 10 91 | result[size-i-1] = byte(r) + '0' 92 | x /= 10 93 | } 94 | return result 95 | } 96 | 97 | func uint2Bytes2(x int) []byte { 98 | // x should between 0 and 59 99 | return uintBytes2[x] 100 | } 101 | 102 | func uint2Bytes4(x int) []byte { 103 | // x should between 1970 and 2038 104 | return uintBytes4[x-1970] 105 | } 106 | 107 | func fastUint2DynamicBytes(x int) []byte { 108 | // x should be uint32 109 | size := 0 110 | switch { 111 | case x < 2: 112 | return []byte{byte(x) + '0'} 113 | case x <= 1000: 114 | return uintBytes[x-2] 115 | case x < 10000: 116 | size = 4 117 | case x < 100000: 118 | size = 5 119 | case x < 1000000: 120 | size = 6 121 | case x < 10000000: 122 | size = 7 123 | case x < 100000000: 124 | size = 8 125 | case x < 1000000000: 126 | size = 9 127 | default: 128 | size = 10 129 | } 130 | result := make([]byte, size) 131 | for i := 0; i < size; i++ { 132 | r := x % 10 133 | result[size-i-1] = byte(r) + '0' 134 | x /= 10 135 | } 136 | return result 137 | } 138 | 139 | func stopTimer(timer *time.Timer) { 140 | if !timer.Stop() { 141 | select { 142 | case <-timer.C: 143 | default: 144 | } 145 | } 146 | } 147 | 148 | func logError(err error) { 149 | if internalLogger != nil { 150 | file, line := Caller(1) 151 | internalLogger.Log(ErrorLevel, file, line, err.Error()) 152 | } 153 | } 154 | 155 | func setNowFunc(nowFunc func() time.Time) { 156 | now = nowFunc 157 | } 158 | 159 | //go:noescape 160 | //go:linkname callers runtime.callers 161 | func callers(skip int, pcbuf []uintptr) int 162 | 163 | // Caller caches the result for runtime.Caller(). 164 | // Inspired by https://zhuanlan.zhihu.com/p/403417640 165 | func Caller(skip int) (file string, line int) { 166 | rpc := [1]uintptr{} 167 | n := callers(skip+1, rpc[:]) 168 | if n < 1 { 169 | return 170 | } 171 | 172 | var frame runtime.Frame 173 | pc := rpc[0] 174 | if f, ok := frameCache.Load(pc); ok { 175 | frame = f.(runtime.Frame) 176 | } else { 177 | frame, _ = runtime.CallersFrames([]uintptr{pc}).Next() 178 | frameCache.Store(pc, frame) 179 | } 180 | return frame.File, frame.Line 181 | } 182 | 183 | // FastTimer is not thread-safe for performance reason, but all the threads will notice its changes in a few milliseconds. 184 | type FastTimer struct { 185 | date string 186 | time string 187 | stopChan chan struct{} 188 | isRunning bool 189 | } 190 | 191 | func (t *FastTimer) update(tm time.Time, buf *bytes.Buffer) { 192 | buf.Reset() 193 | year, mon, day := tm.Date() 194 | buf.Write(uint2Bytes4(year)) 195 | buf.WriteByte('-') 196 | buf.Write(uint2Bytes2(int(mon))) 197 | buf.WriteByte('-') 198 | buf.Write(uint2Bytes2(day)) 199 | t.date = buf.String() 200 | 201 | buf.Reset() 202 | hour, min, sec := tm.Clock() 203 | buf.Write(uint2Bytes2(hour)) 204 | buf.WriteByte(':') 205 | buf.Write(uint2Bytes2(min)) 206 | buf.WriteByte(':') 207 | buf.Write(uint2Bytes2(sec)) 208 | t.time = buf.String() 209 | } 210 | 211 | func (t *FastTimer) start() { 212 | buf := bytes.NewBuffer(make([]byte, 0, dateTimeBufSize)) 213 | t.update(now(), buf) 214 | t.isRunning = true 215 | 216 | t.stopChan = make(chan struct{}) 217 | 218 | go func() { 219 | ticker := time.NewTicker(time.Second) 220 | defer ticker.Stop() 221 | 222 | for { 223 | select { 224 | case tm := <-ticker.C: 225 | t.update(tm, buf) 226 | case <-t.stopChan: 227 | defer func() { recover() }() // t.stopChan might be closed twice during exiting 228 | close(t.stopChan) 229 | return 230 | } 231 | } 232 | }() 233 | } 234 | 235 | func (t *FastTimer) stop() { 236 | if t.isRunning { 237 | defer func() { recover() }() // t.stopChan might be closed during exiting 238 | t.stopChan <- struct{}{} 239 | t.isRunning = false 240 | } 241 | } 242 | 243 | // StartFastTimer starts the fastTimer. 244 | func StartFastTimer() { 245 | fastTimer.start() 246 | } 247 | 248 | // StopFastTimer stops the fastTimer. 249 | func StopFastTimer() { 250 | fastTimer.stop() 251 | } 252 | 253 | //go:noescape 254 | //go:linkname runtime_procPin runtime.procPin 255 | func runtime_procPin() int 256 | 257 | //go:noescape 258 | //go:linkname runtime_procUnpin runtime.procUnpin 259 | func runtime_procUnpin() 260 | -------------------------------------------------------------------------------- /log_test.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | // +build !race 3 | 4 | // golog.FastTimer is not thread-safe. 5 | 6 | package golog 7 | 8 | import ( 9 | "bytes" 10 | "io/ioutil" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | func TestLogger(t *testing.T) { 18 | fastTimer.start() 19 | defer fastTimer.stop() 20 | 21 | infoPath := filepath.Join(os.TempDir(), "test_info.log") 22 | debugPath := filepath.Join(os.TempDir(), "test_debug.log") 23 | os.Remove(infoPath) 24 | os.Remove(debugPath) 25 | 26 | infoWriter, err := NewFileWriter(infoPath) 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | debugWriter, err := NewFileWriter(debugPath) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | 35 | infoHandler := NewHandler(InfoLevel, DefaultFormatter) 36 | infoHandler.AddWriter(infoWriter) 37 | 38 | debugHandler := &Handler{ 39 | formatter: DefaultFormatter, 40 | } 41 | debugHandler.AddWriter(debugWriter) 42 | 43 | l := NewLogger(DebugLevel) 44 | l.AddHandler(infoHandler) 45 | l.AddHandler(debugHandler) 46 | 47 | l.Debugf("test %d", 1) 48 | 49 | stat, err := os.Stat(infoPath) 50 | if err != nil { 51 | t.Error(err) 52 | } 53 | if stat.Size() != 0 { 54 | t.Errorf("file size are %d", stat.Size()) 55 | } 56 | 57 | debugContent, err := ioutil.ReadFile(debugPath) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | size1 := len(debugContent) 62 | if size1 == 0 { 63 | t.Error("debug log is empty") 64 | } 65 | 66 | l.Infof("test %d", 2) 67 | 68 | infoContent, err := ioutil.ReadFile(infoPath) 69 | if err != nil { 70 | t.Error(err) 71 | } 72 | size2 := len(infoContent) 73 | if size2 != size1 { 74 | t.Error("the sizes of debug and info logs are not equal") 75 | } 76 | 77 | parts := strings.Fields(string(infoContent)) 78 | if len(parts) != 6 { 79 | t.Errorf("parts length are %d", len(parts)) 80 | } 81 | if parts[0] != "[I" { 82 | t.Errorf("parts[0] is " + parts[0]) 83 | } 84 | if len(parts[1]) != 10 { 85 | t.Errorf("parts[1] is " + parts[1]) 86 | } 87 | if len(parts[2]) != 8 { 88 | t.Errorf("parts[2] is " + parts[2]) 89 | } 90 | if !strings.HasPrefix(parts[3], "log_test:") { 91 | t.Errorf("parts[3] is " + parts[3]) 92 | } 93 | if parts[4] != "test" { 94 | t.Errorf("parts[4] is " + parts[4]) 95 | } 96 | if parts[5] != "2" { 97 | t.Errorf("parts[5] is " + parts[5]) 98 | } 99 | 100 | debugContent, err = ioutil.ReadFile(debugPath) 101 | if err != nil { 102 | t.Error(err) 103 | } 104 | size3 := len(debugContent) 105 | if size3 == size1*2 { 106 | if !bytes.Equal(debugContent[size1:], infoContent) { 107 | t.Error("log contents are not equal") 108 | } 109 | } else { 110 | t.Errorf("debug log size are %d bytes", size2) 111 | } 112 | 113 | if !bytes.Equal(debugContent[size1:], infoContent) { 114 | t.Error("log contents are not equal") 115 | } 116 | 117 | l.Debug(1) 118 | l.Info(1) 119 | l.Warn(1) 120 | l.Error(1) 121 | l.Crit(1) 122 | l.Warnf("1") 123 | l.Errorf("1") 124 | l.Critf("1") 125 | 126 | infoContent, err = ioutil.ReadFile(infoPath) 127 | if err != nil { 128 | t.Error(err) 129 | } 130 | size4 := len(infoContent) 131 | if size4 <= size2 { 132 | t.Error("info log size not changed") 133 | } 134 | 135 | debugContent, err = ioutil.ReadFile(debugPath) 136 | if err != nil { 137 | t.Error(err) 138 | } 139 | size5 := len(debugContent) 140 | if size5 <= size3 { 141 | t.Error("debug log size not changed") 142 | } 143 | if size5 <= size4 { 144 | t.Error("info log size is larger than debug log size") 145 | } 146 | } 147 | 148 | func TestGetMinLevel(t *testing.T) { 149 | l := NewLogger(InfoLevel) 150 | defer l.Close() 151 | if l.GetMinLevel() != disabledLevel { 152 | t.Errorf("GetMinLevel failed") 153 | } 154 | 155 | errorHandler := NewHandler(ErrorLevel, DefaultFormatter) 156 | l.AddHandler(errorHandler) 157 | if l.GetMinLevel() != ErrorLevel { 158 | t.Errorf("GetMinLevel failed") 159 | } 160 | 161 | debugHandler := NewHandler(DebugLevel, DefaultFormatter) 162 | l.AddHandler(debugHandler) 163 | if l.GetMinLevel() != InfoLevel { 164 | t.Errorf("GetMinLevel failed") 165 | } 166 | } 167 | 168 | func TestAddHandler(t *testing.T) { 169 | w := NewDiscardWriter() 170 | 171 | dh := NewHandler(DebugLevel, DefaultFormatter) 172 | dh.AddWriter(w) 173 | 174 | ih := NewHandler(InfoLevel, DefaultFormatter) 175 | ih.AddWriter(w) 176 | 177 | wh := NewHandler(WarnLevel, DefaultFormatter) 178 | wh.AddWriter(w) 179 | 180 | eh := NewHandler(ErrorLevel, DefaultFormatter) 181 | eh.AddWriter(w) 182 | 183 | ch := NewHandler(CritLevel, DefaultFormatter) 184 | ch.AddWriter(w) 185 | 186 | l := NewLogger(InfoLevel) 187 | if l.IsEnabledFor(CritLevel) { 188 | t.Error("an empty logger should not be enabled for any level") 189 | } 190 | 191 | l.AddHandler(ch) 192 | if !l.IsEnabledFor(CritLevel) { 193 | t.Error("the logger is not enable for critical level") 194 | } 195 | if l.IsEnabledFor(ErrorLevel) { 196 | t.Error("the logger is enable for error level") 197 | } 198 | 199 | l.AddHandler(eh) 200 | if !l.IsEnabledFor(ErrorLevel) { 201 | t.Error("the logger is not enable for error level") 202 | } 203 | 204 | l.AddHandler(wh) 205 | if !l.IsEnabledFor(WarnLevel) { 206 | t.Error("the logger is not enable for warning level") 207 | } 208 | 209 | l.AddHandler(ih) 210 | if !l.IsEnabledFor(InfoLevel) { 211 | t.Error("the logger is not enable for info level") 212 | } 213 | 214 | l.AddHandler(dh) 215 | if l.IsEnabledFor(DebugLevel) { 216 | t.Error("info logger should not enable for debug level") 217 | } 218 | 219 | count := len(l.handlers) 220 | if count != 5 { 221 | t.Errorf("the logger has %d handlers", count) 222 | } 223 | 224 | for i := 0; i < count-1; i++ { 225 | if l.handlers[i].level > l.handlers[i+1].level { 226 | t.Errorf("handlers[%d].level > handlers[%d].level", i, i+1) 227 | } 228 | } 229 | } 230 | 231 | func TestCloseLogger(t *testing.T) { 232 | l := &Logger{} 233 | l.Close() 234 | l.Close() 235 | 236 | l = NewStdoutLogger() 237 | h := l.handlers[0] 238 | w := h.writers[0].(*ConsoleWriter) 239 | l.Close() 240 | if len(l.handlers) > 0 { 241 | t.Error("closed logger is not empty") 242 | } 243 | if len(h.writers) > 0 { 244 | t.Error("closed handler is not empty") 245 | } 246 | if w.File != nil { 247 | t.Error("close logger left its writer opened") 248 | } 249 | l.Close() 250 | } 251 | 252 | func TestNewStdoutLogger(t *testing.T) { 253 | l := NewStdoutLogger() 254 | if l.IsEnabledFor(DebugLevel) { 255 | t.Error("stdout logger is enabled for debug level") 256 | } 257 | if !l.IsEnabledFor(InfoLevel) { 258 | t.Error("stdout logger is not enabled for info level") 259 | } 260 | if !l.IsEnabledFor(ErrorLevel) { 261 | t.Error("stdout logger is not enabled for error level") 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package golog 2 | 3 | import ( 4 | "io" 5 | "sort" 6 | "time" 7 | ) 8 | 9 | // Level specifies the log level. 10 | type Level uint8 11 | 12 | // All the log levels. 13 | const ( 14 | DebugLevel Level = iota 15 | InfoLevel 16 | WarnLevel 17 | ErrorLevel 18 | CritLevel 19 | 20 | disabledLevel Level = 255 21 | ) 22 | 23 | var ( 24 | levelNames = []byte("DIWEC") 25 | 26 | internalLogger *Logger 27 | ) 28 | 29 | // A Record is an item which contains required context for the logger. 30 | type Record struct { 31 | date string 32 | time string 33 | file string 34 | message string 35 | args []interface{} 36 | tm time.Time 37 | line int 38 | level Level 39 | } 40 | 41 | // A Logger is a leveled logger with several handlers. 42 | type Logger struct { 43 | handlers []*Handler 44 | minLevel Level // the min level of the logger and its handlers 45 | level Level // the lowest acceptable level of the logger 46 | isInternal bool 47 | } 48 | 49 | // NewLogger creates a new Logger of the given level. 50 | // Messages with lower level than the logger will be ignored. 51 | func NewLogger(lv Level) *Logger { 52 | return &Logger{level: lv, minLevel: disabledLevel} // disable all levels for empty logger 53 | } 54 | 55 | // AddHandler adds a Handler to the Logger. 56 | func (l *Logger) AddHandler(h *Handler) { 57 | h.isInternal = l.isInternal 58 | l.handlers = append(l.handlers, h) 59 | 60 | if len(l.handlers) > 1 { 61 | sort.Slice(l.handlers, func(i, j int) bool { 62 | return l.handlers[i].level < l.handlers[j].level 63 | }) 64 | } 65 | 66 | minLevel := l.handlers[0].level 67 | if l.level >= minLevel { 68 | l.minLevel = l.level 69 | } else { 70 | l.minLevel = minLevel 71 | } 72 | } 73 | 74 | // IsEnabledFor returns whether it's enabled for the level. 75 | func (l *Logger) IsEnabledFor(level Level) bool { 76 | return l.minLevel <= level 77 | } 78 | 79 | // GetMinLevel returns its minLevel. 80 | // Records lower than its minLevel will be ignored. 81 | func (l *Logger) GetMinLevel() Level { 82 | return l.minLevel 83 | } 84 | 85 | // Log logs a message with context. 86 | // A logger should check the message level before call its Log(). 87 | // The line param should be uint32. 88 | // It's not thread-safe, concurrent messages may be written in a random order 89 | // through different handlers or writers. 90 | // But two messages won't be mixed in a single line. 91 | func (l *Logger) Log(lv Level, file string, line int, msg string, args ...interface{}) { 92 | r := recordPool.Get().(*Record) 93 | r.level = lv 94 | if fastTimer.isRunning { 95 | r.date = fastTimer.date 96 | r.time = fastTimer.time 97 | } else { 98 | r.tm = now() 99 | } 100 | r.file = file 101 | r.line = line 102 | r.message = msg 103 | r.args = args 104 | 105 | for _, h := range l.handlers { 106 | if !h.Handle(r) { 107 | break 108 | } 109 | } 110 | 111 | recordPool.Put(r) 112 | } 113 | 114 | // Close closes its handlers. 115 | // It's safe to call this method more than once. 116 | func (l *Logger) Close() { 117 | for _, h := range l.handlers { 118 | h.Close() 119 | } 120 | l.handlers = nil 121 | } 122 | 123 | // Debug logs a debug level message. It uses fmt.Fprint() to format args. 124 | func (l *Logger) Debug(args ...interface{}) { 125 | if l.IsEnabledFor(DebugLevel) { 126 | file, line := Caller(1) // deeper caller will be more expensive 127 | l.Log(DebugLevel, file, line, "", args...) 128 | } 129 | } 130 | 131 | // Debugf logs a debug level message. It uses fmt.Fprintf() to format msg and args. 132 | func (l *Logger) Debugf(msg string, args ...interface{}) { 133 | if l.IsEnabledFor(DebugLevel) { 134 | file, line := Caller(1) 135 | l.Log(DebugLevel, file, line, msg, args...) 136 | } 137 | } 138 | 139 | // Info logs a info level message. It uses fmt.Fprint() to format args. 140 | func (l *Logger) Info(args ...interface{}) { 141 | if l.IsEnabledFor(InfoLevel) { 142 | file, line := Caller(1) 143 | l.Log(InfoLevel, file, line, "", args...) 144 | } 145 | } 146 | 147 | // Infof logs a info level message. It uses fmt.Fprintf() to format msg and args. 148 | func (l *Logger) Infof(msg string, args ...interface{}) { 149 | if l.IsEnabledFor(InfoLevel) { 150 | file, line := Caller(1) 151 | l.Log(InfoLevel, file, line, msg, args...) 152 | } 153 | } 154 | 155 | // Warn logs a warning level message. It uses fmt.Fprint() to format args. 156 | func (l *Logger) Warn(args ...interface{}) { 157 | if l.IsEnabledFor(WarnLevel) { 158 | file, line := Caller(1) 159 | l.Log(WarnLevel, file, line, "", args...) 160 | } 161 | } 162 | 163 | // Warnf logs a warning level message. It uses fmt.Fprintf() to format msg and args. 164 | func (l *Logger) Warnf(msg string, args ...interface{}) { 165 | if l.IsEnabledFor(WarnLevel) { 166 | file, line := Caller(1) 167 | l.Log(WarnLevel, file, line, msg, args...) 168 | } 169 | } 170 | 171 | // Error logs an error level message. It uses fmt.Fprint() to format args. 172 | func (l *Logger) Error(args ...interface{}) { 173 | if l.IsEnabledFor(ErrorLevel) { 174 | file, line := Caller(1) 175 | l.Log(ErrorLevel, file, line, "", args...) 176 | } 177 | } 178 | 179 | // Errorf logs a error level message. It uses fmt.Fprintf() to format msg and args. 180 | func (l *Logger) Errorf(msg string, args ...interface{}) { 181 | if l.IsEnabledFor(ErrorLevel) { 182 | file, line := Caller(1) 183 | l.Log(ErrorLevel, file, line, msg, args...) 184 | } 185 | } 186 | 187 | // Crit logs a critical level message. It uses fmt.Fprint() to format args. 188 | func (l *Logger) Crit(args ...interface{}) { 189 | if l.IsEnabledFor(CritLevel) { 190 | file, line := Caller(1) 191 | l.Log(CritLevel, file, line, "", args...) 192 | } 193 | } 194 | 195 | // Critf logs a critical level message. It uses fmt.Fprintf() to format msg and args. 196 | func (l *Logger) Critf(msg string, args ...interface{}) { 197 | if l.IsEnabledFor(CritLevel) { 198 | file, line := Caller(1) 199 | l.Log(CritLevel, file, line, msg, args...) 200 | } 201 | } 202 | 203 | // NewLoggerWithWriter creates an info level logger with a writer. 204 | func NewLoggerWithWriter(w io.WriteCloser) *Logger { 205 | h := NewHandler(InfoLevel, DefaultFormatter) 206 | h.AddWriter(w) 207 | l := NewLogger(InfoLevel) 208 | l.AddHandler(h) 209 | return l 210 | } 211 | 212 | // NewStdoutLogger creates a logger with a stdout writer. 213 | func NewStdoutLogger() *Logger { 214 | return NewLoggerWithWriter(NewStdoutWriter()) 215 | } 216 | 217 | // NewStderrLogger creates a logger with a stderr writer. 218 | func NewStderrLogger() *Logger { 219 | return NewLoggerWithWriter(NewStderrWriter()) 220 | } 221 | 222 | // SetInternalLogger sets a logger as the internalLogger which is used to log internal errors. 223 | // The logger and its handlers will be marked as internal, so do not reuse them. 224 | // The internalLogger may discard its own errors to prevent recursive log. 225 | func SetInternalLogger(l *Logger) { 226 | if internalLogger != nil { 227 | internalLogger.isInternal = false 228 | for _, h := range internalLogger.handlers { 229 | h.isInternal = false 230 | } 231 | } 232 | 233 | if l != nil { 234 | l.isInternal = true 235 | for _, h := range l.handlers { 236 | h.isInternal = true 237 | } 238 | } 239 | 240 | internalLogger = l 241 | } 242 | -------------------------------------------------------------------------------- /formatter.go: -------------------------------------------------------------------------------- 1 | package golog 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | var unknownFile = []byte("???") 10 | 11 | var ( 12 | // DefaultFormatter is the default formatter 13 | DefaultFormatter = ParseFormat("[%l %D %T %s] %m") 14 | // TimedRotatingFormatter is a formatter for TimedRotatingFileWriter 15 | TimedRotatingFormatter = ParseFormat("[%l %T %s] %m") 16 | ) 17 | 18 | // A Formatter containing a sequence of FormatParts. 19 | type Formatter struct { 20 | formatParts []FormatPart 21 | } 22 | 23 | // ParseFormat parses a format string into a formatter. 24 | func ParseFormat(format string) (formatter *Formatter) { 25 | if format == "" { 26 | return 27 | } 28 | formatter = &Formatter{} 29 | formatter.findParts([]byte(format)) 30 | formatter.appendByte('\n') 31 | return 32 | } 33 | 34 | /* 35 | Format formats a record to a bytes.Buffer. 36 | Supported format directives: 37 | 38 | %%: % 39 | %l: short name of the level 40 | %T: time string (HH:MM:SS) 41 | %D: date string (YYYY-mm-DD) 42 | %s: source code string (filename:line) 43 | %S: full source code string (/path/filename.go:line) 44 | */ 45 | func (f *Formatter) Format(r *Record, buf *bytes.Buffer) { 46 | for _, part := range f.formatParts { 47 | part.Format(r, buf) 48 | } 49 | } 50 | 51 | func (f *Formatter) findParts(format []byte) { 52 | length := len(format) 53 | index := bytes.IndexByte(format, '%') 54 | if index == -1 || index == length-1 { 55 | if length == 0 { 56 | return 57 | } 58 | if length == 1 { 59 | f.appendByte(format[0]) 60 | } else { 61 | f.appendBytes(format) 62 | } 63 | return 64 | } 65 | 66 | if index > 1 { 67 | f.appendBytes(format[:index]) 68 | } else if index == 1 { 69 | f.appendByte(format[0]) 70 | } 71 | switch c := format[index+1]; c { 72 | case '%': 73 | f.appendByte('%') 74 | case 'l': 75 | f.formatParts = append(f.formatParts, &LevelFormatPart{}) 76 | case 'T': 77 | f.formatParts = append(f.formatParts, &TimeFormatPart{}) 78 | case 'D': 79 | f.formatParts = append(f.formatParts, &DateFormatPart{}) 80 | case 's': 81 | f.formatParts = append(f.formatParts, &SourceFormatPart{}) 82 | case 'S': 83 | f.formatParts = append(f.formatParts, &FullSourceFormatPart{}) 84 | case 'm': 85 | f.formatParts = append(f.formatParts, &MessageFormatPart{}) 86 | default: 87 | f.appendBytes([]byte{'%', c}) 88 | } 89 | f.findParts(format[index+2:]) 90 | return 91 | } 92 | 93 | // FormatPart is an interface containing the Format() method. 94 | type FormatPart interface { 95 | Format(r *Record, buf *bytes.Buffer) 96 | } 97 | 98 | // ByteFormatPart is a FormatPart containing a byte. 99 | type ByteFormatPart struct { 100 | byte byte 101 | } 102 | 103 | // Format writes its byte to the buf. 104 | func (p *ByteFormatPart) Format(r *Record, buf *bytes.Buffer) { 105 | buf.WriteByte(p.byte) 106 | } 107 | 108 | // appendByte appends a byte to the formatter. 109 | // If the previous FormatPart is a ByteFormatPart or BytesFormatPart, they will be merged into a BytesFormatPart; 110 | // otherwise a new ByteFormatPart will be created. 111 | func (f *Formatter) appendByte(b byte) { 112 | parts := f.formatParts 113 | count := len(parts) 114 | if count == 0 { 115 | f.formatParts = append(parts, &ByteFormatPart{byte: b}) 116 | } else { 117 | var p FormatPart 118 | lastPart := parts[count-1] 119 | switch lp := lastPart.(type) { 120 | case *ByteFormatPart: 121 | p = &BytesFormatPart{ 122 | bytes: []byte{lp.byte, b}, 123 | } 124 | case *BytesFormatPart: 125 | p = &BytesFormatPart{ 126 | bytes: append(lp.bytes, b), 127 | } 128 | default: 129 | p = &ByteFormatPart{byte: b} 130 | f.formatParts = append(parts, p) 131 | return 132 | } 133 | f.formatParts[count-1] = p 134 | } 135 | } 136 | 137 | // BytesFormatPart is a FormatPart containing a byte slice. 138 | type BytesFormatPart struct { 139 | bytes []byte 140 | } 141 | 142 | // Format writes its bytes to the buf. 143 | func (p *BytesFormatPart) Format(r *Record, buf *bytes.Buffer) { 144 | buf.Write(p.bytes) 145 | } 146 | 147 | // appendBytes appends a byte slice to the formatter. 148 | // If the previous FormatPart is a ByteFormatPart or BytesFormatPart, they will be merged into a BytesFormatPart; 149 | // otherwise a new BytesFormatPart will be created. 150 | func (f *Formatter) appendBytes(bs []byte) { 151 | parts := f.formatParts 152 | count := len(parts) 153 | if count == 0 { 154 | f.formatParts = append(parts, &BytesFormatPart{bytes: bs}) 155 | } else { 156 | var p FormatPart 157 | lastPart := parts[count-1] 158 | switch lp := lastPart.(type) { 159 | case *ByteFormatPart: // won't reach here 160 | p = &BytesFormatPart{ 161 | bytes: append([]byte{lp.byte}, bs...), 162 | } 163 | case *BytesFormatPart: 164 | p = &BytesFormatPart{ 165 | bytes: append(lp.bytes, bs...), 166 | } 167 | default: 168 | p = &BytesFormatPart{bytes: bs} 169 | f.formatParts = append(parts, p) 170 | return 171 | } 172 | f.formatParts[count-1] = p 173 | } 174 | } 175 | 176 | // LevelFormatPart is a FormatPart of the level placeholder. 177 | type LevelFormatPart struct{} 178 | 179 | // Format writes the short level name of the record to the buf. 180 | func (p *LevelFormatPart) Format(r *Record, buf *bytes.Buffer) { 181 | buf.WriteByte(levelNames[int(r.level)]) 182 | } 183 | 184 | // TimeFormatPart is a FormatPart of the time placeholder. 185 | type TimeFormatPart struct{} 186 | 187 | // Format writes the time string of the record to the buf. 188 | func (p *TimeFormatPart) Format(r *Record, buf *bytes.Buffer) { 189 | if r.time == "" { 190 | hour, min, sec := r.tm.Clock() 191 | buf.Write(uint2Bytes2(hour)) 192 | buf.WriteByte(':') 193 | buf.Write(uint2Bytes2(min)) 194 | buf.WriteByte(':') 195 | buf.Write(uint2Bytes2(sec)) 196 | } else { 197 | buf.WriteString(r.time) 198 | } 199 | } 200 | 201 | // DateFormatPart is a FormatPart of the date placeholder. 202 | type DateFormatPart struct{} 203 | 204 | // Format writes the date string of the record to the buf. 205 | func (p *DateFormatPart) Format(r *Record, buf *bytes.Buffer) { 206 | if r.date == "" { 207 | year, mon, day := r.tm.Date() 208 | buf.Write(uint2Bytes4(year)) 209 | buf.WriteByte('-') 210 | buf.Write(uint2Bytes2(int(mon))) 211 | buf.WriteByte('-') 212 | buf.Write(uint2Bytes2(day)) 213 | } else { 214 | buf.WriteString(r.date) 215 | } 216 | } 217 | 218 | // SourceFormatPart is a FormatPart of the source code placeholder. 219 | type SourceFormatPart struct{} 220 | 221 | // Format writes the source file name and line number of the record to the buf. 222 | func (p *SourceFormatPart) Format(r *Record, buf *bytes.Buffer) { 223 | if r.line > 0 { 224 | length := len(r.file) 225 | if length > 0 { 226 | start := 0 227 | end := length 228 | for i := length - 1; i >= 0; i-- { 229 | c := r.file[i] 230 | if os.IsPathSeparator(c) { 231 | start = i + 1 232 | break 233 | } else if c == '.' { 234 | end = i 235 | } 236 | } 237 | buf.WriteString(r.file[start:end]) 238 | buf.WriteByte(':') 239 | buf.Write(fastUint2DynamicBytes(r.line)) 240 | return 241 | } 242 | } 243 | buf.Write(unknownFile) 244 | } 245 | 246 | // FullSourceFormatPart is a FormatPart of the full source code placeholder. 247 | type FullSourceFormatPart struct{} 248 | 249 | // Format writes the source file path and line number of the record to the buf. 250 | func (p *FullSourceFormatPart) Format(r *Record, buf *bytes.Buffer) { 251 | if r.line > 0 { 252 | buf.WriteString(r.file) 253 | buf.WriteByte(':') 254 | buf.Write(fastUint2DynamicBytes(r.line)) 255 | } else { 256 | buf.Write(unknownFile) 257 | } 258 | } 259 | 260 | // MessageFormatPart is a FormatPart of the message placeholder. 261 | type MessageFormatPart struct{} 262 | 263 | // Format writes the formatted message with args to the buf. 264 | func (p *MessageFormatPart) Format(r *Record, buf *bytes.Buffer) { 265 | if len(r.args) > 0 { 266 | if r.message == "" { 267 | fmt.Fprint(buf, r.args...) 268 | } else { 269 | fmt.Fprintf(buf, r.message, r.args...) 270 | } 271 | } else if r.message != "" { 272 | buf.WriteString(r.message) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /formatter_test.go: -------------------------------------------------------------------------------- 1 | package golog 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestParseFormat(t *testing.T) { 10 | if ParseFormat("") != nil { 11 | t.Error("ParseFormat empty string is not nil") 12 | } 13 | 14 | formatter := ParseFormat("%") 15 | if len(formatter.formatParts) != 1 { 16 | t.Error("ParseFormat % failed") 17 | } 18 | p, ok := formatter.formatParts[0].(*BytesFormatPart) 19 | if !ok { 20 | t.Error("ParseFormat % failed") 21 | } 22 | if string(p.bytes) != "%\n" { 23 | t.Error("ParseFormat % failed") 24 | } 25 | 26 | formatter = ParseFormat("%%") 27 | if len(formatter.formatParts) != 1 { 28 | t.Error("ParseFormat % failed") 29 | } 30 | p, ok = formatter.formatParts[0].(*BytesFormatPart) 31 | if !ok { 32 | t.Error("ParseFormat % failed") 33 | } 34 | if string(p.bytes) != "%\n" { 35 | t.Error("ParseFormat % failed") 36 | } 37 | 38 | formatter = ParseFormat("%a") 39 | if len(formatter.formatParts) != 1 { 40 | t.Error("ParseFormat %a failed") 41 | } 42 | p, ok = formatter.formatParts[0].(*BytesFormatPart) 43 | if !ok { 44 | t.Error("ParseFormat %a failed") 45 | } 46 | if string(p.bytes) != "%a\n" { 47 | t.Error("ParseFormat %a failed") 48 | } 49 | 50 | formatter = ParseFormat("% %") 51 | if len(formatter.formatParts) != 1 { 52 | t.Error("ParseFormat % % failed") 53 | } 54 | p, ok = formatter.formatParts[0].(*BytesFormatPart) 55 | if !ok { 56 | t.Error("ParseFormat % % failed") 57 | } 58 | if string(p.bytes) != "% %\n" { 59 | t.Error("ParseFormat % % failed") 60 | } 61 | 62 | formatter = ParseFormat("% %a") 63 | if len(formatter.formatParts) != 1 { 64 | t.Error("ParseFormat % %a failed") 65 | } 66 | p, ok = formatter.formatParts[0].(*BytesFormatPart) 67 | if !ok { 68 | t.Error("ParseFormat % %a failed") 69 | } 70 | if string(p.bytes) != "% %a\n" { 71 | t.Error("ParseFormat % %a failed") 72 | } 73 | 74 | formatter = ParseFormat("abc") 75 | if len(formatter.formatParts) != 1 { 76 | t.Error("ParseFormat abc failed") 77 | } 78 | p, ok = formatter.formatParts[0].(*BytesFormatPart) 79 | if !ok { 80 | t.Error("ParseFormat abc failed") 81 | } 82 | if string(p.bytes) != "abc\n" { 83 | t.Error("ParseFormat abc failed") 84 | } 85 | 86 | formatter = ParseFormat("%S") 87 | if len(formatter.formatParts) != 2 { 88 | t.Error("ParseFormat abc failed") 89 | } 90 | if _, ok = formatter.formatParts[0].(*FullSourceFormatPart); !ok { 91 | t.Error("ParseFormat %S failed") 92 | } 93 | if _, ok = formatter.formatParts[1].(*ByteFormatPart); !ok { 94 | t.Error("ParseFormat %S failed") 95 | } 96 | 97 | if len(DefaultFormatter.formatParts) != 11 { 98 | t.Errorf("formatParts are %d", len(DefaultFormatter.formatParts)) 99 | } 100 | 101 | part0, ok := DefaultFormatter.formatParts[0].(*ByteFormatPart) 102 | if !ok { 103 | t.Errorf("part0 is " + reflect.TypeOf(DefaultFormatter.formatParts[0]).String()) 104 | } 105 | if part0.byte != '[' { 106 | t.Errorf("byte of part0 is %d", part0.byte) 107 | } 108 | 109 | _, ok = DefaultFormatter.formatParts[1].(*LevelFormatPart) 110 | if !ok { 111 | t.Errorf("part1 is " + reflect.TypeOf(DefaultFormatter.formatParts[1]).String()) 112 | } 113 | 114 | part2, ok := DefaultFormatter.formatParts[2].(*ByteFormatPart) 115 | if !ok { 116 | t.Errorf("part2 is " + reflect.TypeOf(DefaultFormatter.formatParts[2]).String()) 117 | } 118 | if part2.byte != ' ' { 119 | t.Errorf("byte of part2 is %d", part2.byte) 120 | } 121 | 122 | _, ok = DefaultFormatter.formatParts[3].(*DateFormatPart) 123 | if !ok { 124 | t.Errorf("part3 is " + reflect.TypeOf(DefaultFormatter.formatParts[3]).String()) 125 | } 126 | 127 | part4, ok := DefaultFormatter.formatParts[4].(*ByteFormatPart) 128 | if !ok { 129 | t.Errorf("part4 is " + reflect.TypeOf(DefaultFormatter.formatParts[4]).String()) 130 | } 131 | if part4.byte != ' ' { 132 | t.Errorf("byte of part4 is %d", part4.byte) 133 | } 134 | 135 | _, ok = DefaultFormatter.formatParts[5].(*TimeFormatPart) 136 | if !ok { 137 | t.Errorf("part5 is " + reflect.TypeOf(DefaultFormatter.formatParts[5]).String()) 138 | } 139 | 140 | part6, ok := DefaultFormatter.formatParts[6].(*ByteFormatPart) 141 | if !ok { 142 | t.Errorf("part6 is " + reflect.TypeOf(DefaultFormatter.formatParts[6]).String()) 143 | } 144 | if part6.byte != ' ' { 145 | t.Errorf("byte of part6 is %d", part6.byte) 146 | } 147 | 148 | _, ok = DefaultFormatter.formatParts[7].(*SourceFormatPart) 149 | if !ok { 150 | t.Errorf("part7 is " + reflect.TypeOf(DefaultFormatter.formatParts[7]).String()) 151 | } 152 | 153 | part8, ok := DefaultFormatter.formatParts[8].(*BytesFormatPart) 154 | if !ok { 155 | t.Errorf("part8 is " + reflect.TypeOf(DefaultFormatter.formatParts[8]).String()) 156 | } 157 | bs := part8.bytes 158 | if len(bs) != 2 || bs[0] != ']' || bs[1] != ' ' { 159 | t.Errorf("bytes of part8 is " + string(part8.bytes)) 160 | } 161 | 162 | _, ok = DefaultFormatter.formatParts[9].(*MessageFormatPart) 163 | if !ok { 164 | t.Errorf("part9 is " + reflect.TypeOf(DefaultFormatter.formatParts[9]).String()) 165 | } 166 | 167 | part10, ok := DefaultFormatter.formatParts[10].(*ByteFormatPart) 168 | if !ok { 169 | t.Errorf("part10 is " + reflect.TypeOf(DefaultFormatter.formatParts[10]).String()) 170 | } 171 | if part10.byte != '\n' { 172 | t.Errorf("byte of part6 is %d", part6.byte) 173 | } 174 | } 175 | 176 | func TestByteFormatPart(t *testing.T) { 177 | buf := &bytes.Buffer{} 178 | part := ByteFormatPart{'a'} 179 | part.Format(nil, buf) 180 | bs := buf.String() 181 | if bs != "a" { 182 | t.Error() 183 | } 184 | } 185 | 186 | func TestBytesFormatPart(t *testing.T) { 187 | buf := &bytes.Buffer{} 188 | part := BytesFormatPart{[]byte("abc")} 189 | part.Format(nil, buf) 190 | bs := buf.String() 191 | if bs != "abc" { 192 | t.Error() 193 | } 194 | } 195 | 196 | func TestLevelFormatPart(t *testing.T) { 197 | r := &Record{} 198 | buf := &bytes.Buffer{} 199 | part := LevelFormatPart{} 200 | part.Format(r, buf) 201 | bs := buf.String() 202 | if bs != "D" { 203 | t.Error() 204 | } 205 | 206 | r.level = InfoLevel 207 | buf.Reset() 208 | part.Format(r, buf) 209 | bs = buf.String() 210 | if bs != "I" { 211 | t.Error() 212 | } 213 | } 214 | 215 | func TestTimeFormatPart(t *testing.T) { 216 | r := &Record{ 217 | time: "16:12:34", 218 | date: "2018-11-19", 219 | } 220 | buf := &bytes.Buffer{} 221 | part := TimeFormatPart{} 222 | part.Format(r, buf) 223 | bs := buf.String() 224 | if bs != "16:12:34" { 225 | t.Error() 226 | } 227 | } 228 | 229 | func TestDateFormatPart(t *testing.T) { 230 | r := &Record{ 231 | time: "16:12:34", 232 | date: "2018-11-19", 233 | } 234 | buf := &bytes.Buffer{} 235 | part := DateFormatPart{} 236 | part.Format(r, buf) 237 | bs := buf.String() 238 | if bs != "2018-11-19" { 239 | t.Error() 240 | } 241 | } 242 | 243 | func TestSourceFormatPart(t *testing.T) { 244 | r := &Record{} 245 | buf := &bytes.Buffer{} 246 | part := SourceFormatPart{} 247 | part.Format(r, buf) 248 | bs := buf.String() 249 | if bs != string(unknownFile) { 250 | t.Error() 251 | } 252 | 253 | r.file = "/test/test.go" 254 | r.line = 10 255 | buf.Reset() 256 | part.Format(r, buf) 257 | bs = buf.String() 258 | if bs != "test:10" { 259 | t.Error() 260 | } 261 | } 262 | 263 | func TestFullSourceFormatPart(t *testing.T) { 264 | r := &Record{} 265 | buf := &bytes.Buffer{} 266 | part := FullSourceFormatPart{} 267 | part.Format(r, buf) 268 | bs := buf.String() 269 | if bs != string(unknownFile) { 270 | t.Error() 271 | } 272 | 273 | r.file = "/test/test.go" 274 | r.line = 10 275 | buf.Reset() 276 | part.Format(r, buf) 277 | bs = buf.String() 278 | if bs != "/test/test.go:10" { 279 | t.Error() 280 | } 281 | } 282 | 283 | func TestMessageFormatPart(t *testing.T) { 284 | r := &Record{} 285 | buf := &bytes.Buffer{} 286 | part := MessageFormatPart{} 287 | part.Format(r, buf) 288 | bs := buf.String() 289 | if bs != "" { 290 | t.Error() 291 | } 292 | 293 | r.message = "abc" 294 | buf.Reset() 295 | part.Format(r, buf) 296 | bs = buf.String() 297 | if bs != "abc" { 298 | t.Error() 299 | } 300 | 301 | r.message = "abc %d %d" 302 | r.args = []interface{}{1, 2} 303 | buf.Reset() 304 | part.Format(r, buf) 305 | bs = buf.String() 306 | if bs != "abc 1 2" { 307 | t.Error() 308 | } 309 | 310 | r.message = "" 311 | r.args = []interface{}{1, 2} 312 | buf.Reset() 313 | part.Format(r, buf) 314 | bs = buf.String() 315 | if bs != "1 2" { 316 | t.Error() 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package golog 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | const maxRetryCount = 10 17 | 18 | func checkFileSize(t *testing.T, path string, size int64) { 19 | stat, err := os.Stat(path) 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | if stat.Size() != size { 25 | t.Fatalf("file size are %d bytes", stat.Size()) 26 | } 27 | } 28 | 29 | func checkFileSizeN(t *testing.T, path string, size int64) { 30 | for i := 0; i < maxRetryCount; i++ { 31 | time.Sleep(flushDuration) 32 | 33 | stat, err := os.Stat(path) 34 | if err != nil { 35 | if i == maxRetryCount-1 { 36 | t.Error(err) 37 | } else { 38 | continue 39 | } 40 | } 41 | 42 | if stat.Size() != size { 43 | if i == maxRetryCount-1 { 44 | t.Errorf("file size are %d bytes", stat.Size()) 45 | } else { 46 | continue 47 | } 48 | } else { 49 | break 50 | } 51 | } 52 | } 53 | 54 | func TestMain(m *testing.M) { 55 | SetInternalLogger(NewStderrLogger()) 56 | os.Exit(m.Run()) 57 | } 58 | 59 | func TestBufferedFileWriter(t *testing.T) { 60 | const bufferSize = 1024 61 | 62 | path := filepath.Join(os.TempDir(), "test.log") 63 | os.Remove(path) 64 | w, err := NewBufferedFileWriter(path, BufferSize(bufferSize)) 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | 69 | f, err := os.Open(path) 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | stat, err := f.Stat() 74 | if err != nil { 75 | t.Error(err) 76 | } 77 | if stat.Size() != 0 { 78 | t.Errorf("file size are %d bytes", stat.Size()) 79 | } 80 | 81 | n, err := w.Write([]byte("test")) 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | if n != 4 { 86 | t.Errorf("write %d bytes, expected 4", n) 87 | } 88 | 89 | buf := make([]byte, bufferSize*2) 90 | 91 | for i := 0; i < maxRetryCount; i++ { 92 | n, err = f.Read(buf) 93 | if err != nil { 94 | if i == maxRetryCount-1 { 95 | t.Error(err) 96 | } else { 97 | time.Sleep(flushDuration) 98 | continue 99 | } 100 | } else { 101 | break 102 | } 103 | } 104 | if n != 4 { 105 | t.Errorf("read %d bytes, expected 4", n) 106 | } 107 | bs := string(buf[:4]) 108 | if bs != "test" { 109 | t.Error("read bytes are " + bs) 110 | } 111 | 112 | for i := 0; i < bufferSize; i++ { 113 | w.Write([]byte{'1'}) 114 | } 115 | w.Write([]byte{'2'}) // writes over bufferSize cause flushing 116 | n, err = f.Read(buf) 117 | if err != nil { 118 | t.Error(err) 119 | } 120 | if n != bufferSize { 121 | t.Errorf("read %d bytes", n) 122 | } 123 | if buf[bufferSize-1] != '1' { 124 | t.Errorf("last byte is %d", buf[bufferSize-1]) 125 | } 126 | if buf[bufferSize] != 0 { 127 | t.Errorf("next byte is %d", buf[bufferSize-1]) 128 | } 129 | 130 | for i := 0; i < maxRetryCount; i++ { 131 | n, err = f.Read(buf) 132 | if err != nil { 133 | if i == maxRetryCount-1 { 134 | t.Error(err) 135 | } else { 136 | time.Sleep(flushDuration) 137 | continue 138 | } 139 | } else { 140 | break 141 | } 142 | } 143 | 144 | if n != 1 { 145 | t.Errorf("read %d bytes", n) 146 | } 147 | if buf[0] != '2' { 148 | t.Errorf("first byte is %d", buf[0]) 149 | } 150 | if buf[1] != '1' { 151 | t.Errorf("next byte is %d", buf[1]) 152 | } 153 | 154 | f.Close() 155 | w.Close() 156 | } 157 | 158 | func TestRotatingFileWriter(t *testing.T) { 159 | dir := filepath.Join(os.TempDir(), "test") 160 | path := filepath.Join(dir, "test.log") 161 | err := os.RemoveAll(dir) 162 | if err != nil { 163 | t.Error(err) 164 | } 165 | err = os.Mkdir(dir, 0755) 166 | if err != nil { 167 | t.Error(err) 168 | } 169 | 170 | _, err = NewRotatingFileWriter(path, 0, 2) 171 | if err == nil { 172 | t.Errorf("NewRotatingFileWriter with maxSize 0 is invalid") 173 | } 174 | 175 | _, err = NewRotatingFileWriter(path, 128, 0) 176 | if err == nil { 177 | t.Errorf("NewRotatingFileWriter with backupCount 0 is invalid") 178 | } 179 | 180 | w, err := NewRotatingFileWriter(path, 128, 2) 181 | if err != nil { 182 | t.Error(err) 183 | } 184 | stat, err := os.Stat(path) 185 | if err != nil { 186 | t.Error(err) 187 | } 188 | if stat.Size() != 0 { 189 | t.Errorf("file size are %d bytes", stat.Size()) 190 | } 191 | 192 | bs := []byte("0123456789") 193 | for i := 0; i < 20; i++ { 194 | w.Write(bs) 195 | } 196 | 197 | checkFileSize(t, path, 0) 198 | 199 | checkFileSize(t, path+".1", 130) 200 | 201 | _, err = os.Stat(path + ".2") 202 | if !os.IsNotExist(err) { 203 | t.Error(err) 204 | } 205 | 206 | checkFileSizeN(t, path, 70) 207 | 208 | // second write 209 | for i := 0; i < 20; i++ { 210 | w.Write(bs) 211 | } 212 | 213 | checkFileSize(t, path, 0) 214 | checkFileSize(t, path+".1", 130) 215 | checkFileSize(t, path+".2", 130) 216 | checkFileSizeN(t, path, 10) 217 | 218 | bs = make([]byte, 200) 219 | for i := 0; i < 200; i++ { 220 | bs[i] = '1' 221 | } 222 | w.Write(bs) 223 | 224 | checkFileSize(t, path, 0) 225 | checkFileSize(t, path+".1", 210) 226 | checkFileSize(t, path+".2", 130) 227 | checkFileSizeN(t, path, 0) 228 | 229 | w.Write(bs) 230 | 231 | checkFileSize(t, path, 0) 232 | checkFileSize(t, path+".1", 200) 233 | checkFileSize(t, path+".2", 210) 234 | checkFileSizeN(t, path, 0) 235 | 236 | w.Close() 237 | } 238 | 239 | func TestTimedRotatingFileWriterByDate(t *testing.T) { 240 | dir := filepath.Join(os.TempDir(), "test") 241 | pathPrefix := filepath.Join(dir, "test") 242 | err := os.RemoveAll(dir) 243 | if err != nil { 244 | t.Error(err) 245 | } 246 | err = os.Mkdir(dir, 0755) 247 | if err != nil { 248 | t.Error(err) 249 | } 250 | 251 | tm := time.Date(2018, 11, 19, 16, 12, 34, 56, time.Local) 252 | var lock sync.RWMutex 253 | setNowFunc(func() time.Time { 254 | lock.RLock() 255 | now := tm 256 | lock.RUnlock() 257 | return now 258 | }) 259 | var setNow = func(now time.Time) { 260 | lock.Lock() 261 | tm = now 262 | lock.Unlock() 263 | } 264 | 265 | oldNextRotateDuration := nextRotateDuration 266 | nextRotateDuration = func(rotateDuration RotateDuration) time.Duration { 267 | return flushDuration * 3 268 | } 269 | 270 | _, err = NewTimedRotatingFileWriter(pathPrefix, RotateByDate, 0) 271 | if err == nil { 272 | t.Errorf("NewTimedRotatingFileWriter with backupCount 0 is invalid") 273 | } 274 | 275 | w, err := NewTimedRotatingFileWriter(pathPrefix, RotateByDate, 2) 276 | if err != nil { 277 | t.Error(err) 278 | } 279 | path := pathPrefix + "-20181119.log" 280 | checkFileSize(t, path, 0) 281 | 282 | w.Write([]byte("123")) 283 | checkFileSize(t, path, 0) 284 | 285 | setNow(time.Date(2018, 11, 20, 16, 12, 34, 56, time.Local)) 286 | time.Sleep(flushDuration * 2) 287 | checkFileSizeN(t, path, 3) 288 | 289 | time.Sleep(flushDuration * 2) 290 | path = pathPrefix + "-20181120.log" 291 | checkFileSizeN(t, path, 0) 292 | 293 | w.Write([]byte("4567")) 294 | setNow(time.Date(2018, 11, 21, 16, 12, 34, 56, time.Local)) 295 | 296 | time.Sleep(flushDuration * 2) 297 | checkFileSizeN(t, path, 4) 298 | 299 | time.Sleep(flushDuration * 3) 300 | checkFileSizeN(t, path, 4) 301 | checkFileSizeN(t, pathPrefix+"-20181121.log", 0) 302 | 303 | setNow(time.Date(2018, 11, 22, 16, 12, 34, 56, time.Local)) 304 | time.Sleep(flushDuration * 3) 305 | checkFileSizeN(t, pathPrefix+"-20181121.log", 0) 306 | _, err = os.Stat(pathPrefix + "-20181119.log") 307 | if !os.IsNotExist(err) { 308 | t.Error(err) 309 | } 310 | 311 | w.Close() 312 | setNowFunc(time.Now) 313 | nextRotateDuration = oldNextRotateDuration 314 | } 315 | 316 | func TestTimedRotatingFileWriterByHour(t *testing.T) { 317 | dir := filepath.Join(os.TempDir(), "test") 318 | pathPrefix := filepath.Join(dir, "test") 319 | err := os.RemoveAll(dir) 320 | if err != nil { 321 | t.Error(err) 322 | } 323 | err = os.Mkdir(dir, 0755) 324 | if err != nil { 325 | t.Error(err) 326 | } 327 | 328 | tm := time.Date(2018, 11, 19, 16, 12, 34, 56, time.Local) 329 | var lock sync.RWMutex 330 | setNowFunc(func() time.Time { 331 | lock.RLock() 332 | now := tm 333 | lock.RUnlock() 334 | return now 335 | }) 336 | var setNow = func(now time.Time) { 337 | lock.Lock() 338 | tm = now 339 | lock.Unlock() 340 | } 341 | 342 | oldNextRotateDuration := nextRotateDuration 343 | nextRotateDuration = func(rotateDuration RotateDuration) time.Duration { 344 | return flushDuration * 3 345 | } 346 | 347 | w, err := NewTimedRotatingFileWriter(pathPrefix, RotateByHour, 2) 348 | if err != nil { 349 | t.Error(err) 350 | } 351 | path := pathPrefix + "-2018111916.log" 352 | checkFileSize(t, path, 0) 353 | 354 | w.Write([]byte("123")) 355 | checkFileSize(t, path, 0) 356 | 357 | setNow(time.Date(2018, 11, 19, 17, 12, 34, 56, time.Local)) 358 | time.Sleep(flushDuration * 3) 359 | checkFileSizeN(t, path, 3) 360 | 361 | time.Sleep(flushDuration * 3) 362 | path = pathPrefix + "-2018111917.log" 363 | checkFileSizeN(t, path, 0) 364 | 365 | w.Write([]byte("4567")) 366 | setNow(time.Date(2018, 11, 19, 18, 12, 34, 56, time.Local)) 367 | time.Sleep(flushDuration * 3) 368 | checkFileSizeN(t, path, 4) 369 | checkFileSizeN(t, pathPrefix+"-2018111918.log", 0) 370 | 371 | setNow(time.Date(2018, 11, 22, 16, 12, 34, 56, time.Local)) 372 | time.Sleep(flushDuration * 3) 373 | checkFileSizeN(t, pathPrefix+"-2018112216.log", 0) 374 | _, err = os.Stat(pathPrefix + "-2018111916.log") 375 | if !os.IsNotExist(err) { 376 | t.Error(err) 377 | } 378 | 379 | w.Close() 380 | setNowFunc(time.Now) 381 | nextRotateDuration = oldNextRotateDuration 382 | } 383 | 384 | type badWriter struct{} 385 | 386 | func (w *badWriter) Write(p []byte) (n int, err error) { 387 | return 0, io.ErrShortWrite 388 | } 389 | 390 | func (w *badWriter) Close() error { 391 | return nil 392 | } 393 | 394 | func TestBadWriter(t *testing.T) { 395 | path := filepath.Join(os.TempDir(), "error.log") 396 | os.Remove(path) 397 | w, err := NewBufferedFileWriter(path) 398 | if err != nil { 399 | t.Error(err) 400 | } 401 | 402 | newLogger := NewLoggerWithWriter(w) 403 | oldLogger := internalLogger 404 | SetInternalLogger(newLogger) 405 | defer SetInternalLogger(oldLogger) 406 | 407 | l := NewLoggerWithWriter(&badWriter{}) 408 | l.Log(InfoLevel, "", 0, "test") 409 | l.Close() 410 | 411 | time.Sleep(flushDuration * 2) 412 | 413 | content, err := ioutil.ReadFile(path) 414 | if err != nil { 415 | t.Error(err) 416 | } 417 | size := len(content) 418 | if size == 0 { 419 | t.Error("log is empty") 420 | return 421 | } 422 | 423 | if !strings.Contains(string(content), io.ErrShortWrite.Error()) { 424 | t.Error("bad writer raised no error") 425 | return 426 | } 427 | } 428 | 429 | func TestNextRotateDuration(t *testing.T) { 430 | if nextRotateDuration(RotateByDate) > time.Hour*24 { 431 | t.Errorf("nextRotateDuration(RotateByDate) longer than 1 day") 432 | } 433 | if nextRotateDuration(RotateByHour) > time.Hour { 434 | t.Errorf("nextRotateDuration(RotateByHour) longer than 1 hour") 435 | } 436 | } 437 | 438 | func TestConcurrentFileWriter(t *testing.T) { 439 | path := filepath.Join(os.TempDir(), "test.log") 440 | os.Remove(path) 441 | w, err := NewConcurrentFileWriter(path, BufferSize(1024*1024)) 442 | if err != nil { 443 | t.Error(err) 444 | } 445 | 446 | f, err := os.Open(path) 447 | if err != nil { 448 | t.Error(err) 449 | } 450 | stat, err := f.Stat() 451 | if err != nil { 452 | t.Error(err) 453 | } 454 | if stat.Size() != 0 { 455 | t.Errorf("file size are %d bytes", stat.Size()) 456 | } 457 | 458 | n, err := w.Write([]byte("test")) 459 | if err != nil { 460 | t.Error(err) 461 | } 462 | if n != 4 { 463 | t.Errorf("write %d bytes, expected 4", n) 464 | } 465 | 466 | buf := make([]byte, defaultBufferSize) 467 | for i := 0; i < maxRetryCount; i++ { 468 | n, err = f.Read(buf) 469 | if err != nil { 470 | if i == maxRetryCount-1 { 471 | t.Error(err) 472 | } else { 473 | time.Sleep(flushDuration) 474 | continue 475 | } 476 | } else { 477 | break 478 | } 479 | } 480 | if n != 4 { 481 | t.Errorf("read %d bytes, expected 4", n) 482 | } 483 | bs := string(buf[:4]) 484 | if bs != "test" { 485 | t.Error("read bytes are " + bs) 486 | } 487 | 488 | var count = w.cpuCount 489 | if count < 4 { 490 | count = 4 491 | } else if count > 10 { 492 | count = 10 493 | } 494 | 495 | wg := sync.WaitGroup{} 496 | wg.Add(count) 497 | const writeCount = 10000 498 | var dataSize int 499 | for i := 0; i < count; i++ { 500 | data := []byte("test" + strconv.Itoa(i) + "\n") 501 | dataSize = len(data) 502 | go func() { 503 | for j := 0; j < writeCount; j++ { 504 | w.Write(data) 505 | } 506 | wg.Done() 507 | }() 508 | } 509 | wg.Wait() 510 | 511 | for i := 0; i < maxRetryCount; i++ { 512 | time.Sleep(flushDuration) 513 | n, err = f.Read(buf) 514 | if err != nil { 515 | if i == maxRetryCount-1 { 516 | t.Error(err) 517 | } else { 518 | continue 519 | } 520 | } else { 521 | break 522 | } 523 | } 524 | if n != count*dataSize*writeCount { 525 | t.Errorf("read %d bytes, expected %d bytes", n, count*dataSize*writeCount) 526 | } 527 | 528 | lines := bytes.Split(buf[:n], []byte{'\n'}) 529 | if len(lines) != count*writeCount+1 { 530 | t.Errorf("read %d lines, expected %d lines", len(lines), count*writeCount+1) 531 | } 532 | if len(lines[count*writeCount]) != 0 { 533 | t.Error("last part is not empty") 534 | } 535 | lines = lines[:count*writeCount] 536 | for i, line := range lines { 537 | if len(line) != dataSize-1 { 538 | t.Errorf("length of line %d is %d, expected %d", i, len(line), dataSize-1) 539 | } 540 | } 541 | 542 | f.Close() 543 | w.Close() 544 | } 545 | -------------------------------------------------------------------------------- /log/log_test.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | // +build !race 3 | 4 | // golog.FastTimer is not thread-safe. 5 | 6 | package log 7 | 8 | import ( 9 | "bytes" 10 | "errors" 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | 15 | "github.com/keakon/golog" 16 | ) 17 | 18 | type memoryWriter struct { 19 | bytes.Buffer 20 | } 21 | 22 | func (w *memoryWriter) Close() error { 23 | w.Buffer.Reset() 24 | return nil 25 | } 26 | 27 | func errorFunc(args ...interface{}) { 28 | if len(args) == 1 { 29 | arg := args[0] 30 | if _, ok := arg.(error); ok { 31 | // skip 32 | return 33 | } 34 | } 35 | file, line := golog.Caller(1) 36 | defaultLogger.Log(golog.ErrorLevel, file, line, "", args...) 37 | } 38 | 39 | func errorfFunc(msg string, args ...interface{}) { 40 | if len(args) == 1 { 41 | arg := args[0] 42 | if _, ok := arg.(error); ok { 43 | // skip 44 | return 45 | } 46 | } 47 | file, line := golog.Caller(1) 48 | defaultLogger.Log(golog.ErrorLevel, file, line, msg, args...) 49 | } 50 | 51 | func TestSetLogFunc(t *testing.T) { 52 | golog.StartFastTimer() 53 | defer golog.StopFastTimer() 54 | 55 | e := errors.New("test") 56 | w := &memoryWriter{} 57 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 58 | h.AddWriter(w) 59 | l := golog.NewLogger(golog.InfoLevel) 60 | l.AddHandler(h) 61 | 62 | SetDefaultLogger(l) 63 | SetLogFunc(errorFunc, golog.ErrorLevel) 64 | SetLogfFunc(errorfFunc, golog.ErrorLevel) 65 | 66 | Debug("test") 67 | if w.Buffer.Len() != 0 { 68 | t.Error("memoryWriter is not empty") 69 | w.Buffer.Reset() 70 | } 71 | 72 | Error(e) 73 | if w.Buffer.Len() != 0 { 74 | t.Error("memoryWriter is not empty") 75 | w.Buffer.Reset() 76 | } 77 | 78 | Error("test") 79 | if w.Buffer.Len() == 0 { 80 | t.Error("memoryWriter is empty") 81 | } 82 | w.Buffer.Reset() 83 | 84 | Errorf("error: %v", e) 85 | if w.Buffer.Len() != 0 { 86 | t.Error("memoryWriter is not empty") 87 | w.Buffer.Reset() 88 | } 89 | 90 | Errorf("error: %s", "test") 91 | if w.Buffer.Len() == 0 { 92 | t.Error("memoryWriter is empty") 93 | } 94 | w.Buffer.Reset() 95 | 96 | SetLogFunc(errorFunc, golog.DebugLevel) 97 | SetLogfFunc(errorfFunc, golog.DebugLevel) 98 | 99 | for level := golog.DebugLevel; level <= golog.CritLevel; level++ { 100 | SetLogFunc(errorFunc, level) 101 | SetLogfFunc(errorfFunc, level) 102 | } 103 | 104 | Debug(e) 105 | if w.Buffer.Len() != 0 { 106 | t.Error("memoryWriter is not empty") 107 | w.Buffer.Reset() 108 | } 109 | 110 | Debug("test") 111 | if w.Buffer.Len() == 0 { 112 | t.Error("memoryWriter is empty") 113 | } 114 | w.Buffer.Reset() 115 | 116 | Debugf("error: %v", e) 117 | if w.Buffer.Len() != 0 { 118 | t.Error("memoryWriter is not empty") 119 | w.Buffer.Reset() 120 | } 121 | 122 | Debugf("error: %s", "test") 123 | if w.Buffer.Len() == 0 { 124 | t.Error("memoryWriter is empty") 125 | } 126 | w.Buffer.Reset() 127 | 128 | Info(e) 129 | if w.Buffer.Len() != 0 { 130 | t.Error("memoryWriter is not empty") 131 | w.Buffer.Reset() 132 | } 133 | 134 | Info("test") 135 | if w.Buffer.Len() == 0 { 136 | t.Error("memoryWriter is empty") 137 | } 138 | w.Buffer.Reset() 139 | 140 | Infof("error: %v", e) 141 | if w.Buffer.Len() != 0 { 142 | t.Error("memoryWriter is not empty") 143 | w.Buffer.Reset() 144 | } 145 | 146 | Infof("error: %s", "test") 147 | if w.Buffer.Len() == 0 { 148 | t.Error("memoryWriter is empty") 149 | } 150 | w.Buffer.Reset() 151 | 152 | Crit(e) 153 | if w.Buffer.Len() != 0 { 154 | t.Error("memoryWriter is not empty") 155 | w.Buffer.Reset() 156 | } 157 | 158 | Crit("test") 159 | if w.Buffer.Len() == 0 { 160 | t.Error("memoryWriter is empty") 161 | } 162 | w.Buffer.Reset() 163 | 164 | Critf("error: %v", e) 165 | if w.Buffer.Len() != 0 { 166 | t.Error("memoryWriter is not empty") 167 | w.Buffer.Reset() 168 | } 169 | 170 | Critf("error: %s", "test") 171 | if w.Buffer.Len() == 0 { 172 | t.Error("memoryWriter is empty") 173 | } 174 | w.Buffer.Reset() 175 | 176 | l.Close() 177 | } 178 | 179 | func TestLogFuncs(t *testing.T) { 180 | golog.StartFastTimer() 181 | defer golog.StopFastTimer() 182 | 183 | w := &memoryWriter{} 184 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 185 | h.AddWriter(w) 186 | l := golog.NewLogger(golog.InfoLevel) 187 | l.AddHandler(h) 188 | SetDefaultLogger(l) 189 | 190 | Debug("test") 191 | if w.Buffer.Len() != 0 { 192 | t.Error("memoryWriter is not empty") 193 | } 194 | Debugf("test") 195 | if w.Buffer.Len() != 0 { 196 | t.Error("memoryWriter is not empty") 197 | } 198 | 199 | Info("test") 200 | if w.Buffer.Len() == 0 { 201 | t.Error("memoryWriter is empty") 202 | } 203 | w.Buffer.Reset() 204 | 205 | Infof("test") 206 | if w.Buffer.Len() == 0 { 207 | t.Error("memoryWriter is empty") 208 | } 209 | w.Buffer.Reset() 210 | 211 | Warn("test") 212 | if w.Buffer.Len() == 0 { 213 | t.Error("memoryWriter is empty") 214 | } 215 | w.Buffer.Reset() 216 | 217 | Warnf("test") 218 | if w.Buffer.Len() == 0 { 219 | t.Error("memoryWriter is empty") 220 | } 221 | w.Buffer.Reset() 222 | 223 | Error("test") 224 | if w.Buffer.Len() == 0 { 225 | t.Error("memoryWriter is empty") 226 | } 227 | w.Buffer.Reset() 228 | 229 | Errorf("test") 230 | if w.Buffer.Len() == 0 { 231 | t.Error("memoryWriter is empty") 232 | } 233 | 234 | Crit("test") 235 | if w.Buffer.Len() == 0 { 236 | t.Error("memoryWriter is empty") 237 | } 238 | w.Buffer.Reset() 239 | 240 | Critf("test") 241 | if w.Buffer.Len() == 0 { 242 | t.Error("memoryWriter is empty") 243 | } 244 | l.Close() 245 | 246 | h = golog.NewHandler(golog.ErrorLevel, golog.DefaultFormatter) 247 | h.AddWriter(w) 248 | l = golog.NewLogger(golog.ErrorLevel) 249 | l.AddHandler(h) 250 | SetDefaultLogger(l) 251 | 252 | Info("test") 253 | if w.Buffer.Len() != 0 { 254 | t.Error("memoryWriter is not empty") 255 | } 256 | w.Buffer.Reset() 257 | 258 | Error("test") 259 | if w.Buffer.Len() == 0 { 260 | t.Error("memoryWriter is empty") 261 | } 262 | l.Close() 263 | } 264 | 265 | func TestIsEnabledFor(t *testing.T) { 266 | SetDefaultLogger(nil) 267 | if IsEnabledFor(golog.DebugLevel) { 268 | t.Error("nil logger is enabled for debug level") 269 | } 270 | 271 | l := golog.NewStdoutLogger() 272 | SetDefaultLogger(l) 273 | if IsEnabledFor(golog.DebugLevel) { 274 | t.Error("info logger is enabled for debug level") 275 | } 276 | if !IsEnabledFor(golog.InfoLevel) { 277 | t.Error("info logger is disabled for info level") 278 | } 279 | if !IsEnabledFor(golog.ErrorLevel) { 280 | t.Error("info logger is disabled for error level") 281 | } 282 | } 283 | 284 | func BenchmarkDiscardLogger(b *testing.B) { 285 | golog.StartFastTimer() 286 | defer golog.StopFastTimer() 287 | 288 | w := golog.NewDiscardWriter() 289 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 290 | h.AddWriter(w) 291 | l := golog.NewLogger(golog.InfoLevel) 292 | l.AddHandler(h) 293 | SetDefaultLogger(l) 294 | 295 | b.ResetTimer() 296 | 297 | for i := 0; i < b.N; i++ { 298 | Infof("test") 299 | } 300 | l.Close() 301 | } 302 | 303 | func BenchmarkDiscardLoggerParallel(b *testing.B) { 304 | golog.StartFastTimer() 305 | defer golog.StopFastTimer() 306 | 307 | w := golog.NewDiscardWriter() 308 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 309 | h.AddWriter(w) 310 | l := golog.NewLogger(golog.InfoLevel) 311 | l.AddHandler(h) 312 | SetDefaultLogger(l) 313 | 314 | b.ResetTimer() 315 | 316 | b.RunParallel(func(pb *testing.PB) { 317 | for pb.Next() { 318 | Infof("test") 319 | } 320 | }) 321 | l.Close() 322 | } 323 | 324 | func BenchmarkDiscardLoggerWithoutTimer(b *testing.B) { 325 | w := golog.NewDiscardWriter() 326 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 327 | h.AddWriter(w) 328 | l := golog.NewLogger(golog.InfoLevel) 329 | l.AddHandler(h) 330 | SetDefaultLogger(l) 331 | 332 | b.ResetTimer() 333 | 334 | for i := 0; i < b.N; i++ { 335 | Infof("test") 336 | } 337 | l.Close() 338 | } 339 | 340 | func BenchmarkDiscardLoggerWithoutTimerParallel(b *testing.B) { 341 | w := golog.NewDiscardWriter() 342 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 343 | h.AddWriter(w) 344 | l := golog.NewLogger(golog.InfoLevel) 345 | l.AddHandler(h) 346 | SetDefaultLogger(l) 347 | 348 | b.ResetTimer() 349 | 350 | b.RunParallel(func(pb *testing.PB) { 351 | for pb.Next() { 352 | Infof("test") 353 | } 354 | }) 355 | l.Close() 356 | } 357 | 358 | func BenchmarkNopLog(b *testing.B) { 359 | w := golog.NewDiscardWriter() 360 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 361 | h.AddWriter(w) 362 | l := golog.NewLogger(golog.InfoLevel) 363 | l.AddHandler(h) 364 | SetDefaultLogger(l) 365 | 366 | b.ResetTimer() 367 | 368 | for i := 0; i < b.N; i++ { 369 | Debugf("test") 370 | } 371 | l.Close() 372 | } 373 | 374 | func BenchmarkNopLogParallel(b *testing.B) { 375 | w := golog.NewDiscardWriter() 376 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 377 | h.AddWriter(w) 378 | l := golog.NewLogger(golog.InfoLevel) 379 | l.AddHandler(h) 380 | SetDefaultLogger(l) 381 | 382 | b.ResetTimer() 383 | 384 | b.RunParallel(func(pb *testing.PB) { 385 | for pb.Next() { 386 | Debugf("test") 387 | } 388 | }) 389 | l.Close() 390 | } 391 | 392 | func BenchmarkMultiLevels(b *testing.B) { 393 | golog.StartFastTimer() 394 | defer golog.StopFastTimer() 395 | 396 | w := golog.NewDiscardWriter() 397 | dh := golog.NewHandler(golog.DebugLevel, golog.DefaultFormatter) 398 | dh.AddWriter(w) 399 | ih := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 400 | ih.AddWriter(w) 401 | wh := golog.NewHandler(golog.WarnLevel, golog.DefaultFormatter) 402 | wh.AddWriter(w) 403 | eh := golog.NewHandler(golog.ErrorLevel, golog.DefaultFormatter) 404 | eh.AddWriter(w) 405 | ch := golog.NewHandler(golog.CritLevel, golog.DefaultFormatter) 406 | ch.AddWriter(w) 407 | 408 | l := golog.NewLogger(golog.WarnLevel) 409 | l.AddHandler(dh) 410 | l.AddHandler(ih) 411 | l.AddHandler(wh) 412 | l.AddHandler(eh) 413 | l.AddHandler(ch) 414 | SetDefaultLogger(l) 415 | 416 | b.ResetTimer() 417 | 418 | for i := 0; i < b.N; i++ { 419 | Debugf("test") 420 | Infof("test") 421 | Warnf("test") 422 | Errorf("test") 423 | Critf("test") 424 | } 425 | l.Close() 426 | } 427 | 428 | func BenchmarkMultiLevelsParallel(b *testing.B) { 429 | golog.StartFastTimer() 430 | defer golog.StopFastTimer() 431 | 432 | w := golog.NewDiscardWriter() 433 | dh := golog.NewHandler(golog.DebugLevel, golog.DefaultFormatter) 434 | dh.AddWriter(w) 435 | ih := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 436 | ih.AddWriter(w) 437 | wh := golog.NewHandler(golog.WarnLevel, golog.DefaultFormatter) 438 | wh.AddWriter(w) 439 | eh := golog.NewHandler(golog.ErrorLevel, golog.DefaultFormatter) 440 | eh.AddWriter(w) 441 | ch := golog.NewHandler(golog.CritLevel, golog.DefaultFormatter) 442 | ch.AddWriter(w) 443 | 444 | l := golog.NewLogger(golog.WarnLevel) 445 | l.AddHandler(dh) 446 | l.AddHandler(ih) 447 | l.AddHandler(wh) 448 | l.AddHandler(eh) 449 | l.AddHandler(ch) 450 | SetDefaultLogger(l) 451 | 452 | b.ResetTimer() 453 | 454 | b.RunParallel(func(pb *testing.PB) { 455 | for pb.Next() { 456 | Debugf("test") 457 | Infof("test") 458 | Warnf("test") 459 | Errorf("test") 460 | Critf("test") 461 | } 462 | }) 463 | l.Close() 464 | } 465 | 466 | func BenchmarkBufferedFileLogger(b *testing.B) { 467 | golog.StartFastTimer() 468 | defer golog.StopFastTimer() 469 | 470 | path := filepath.Join(os.TempDir(), "test.log") 471 | os.Remove(path) 472 | w, err := golog.NewBufferedFileWriter(path) 473 | if err != nil { 474 | b.Error(err) 475 | } 476 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 477 | h.AddWriter(w) 478 | l := golog.NewLogger(golog.InfoLevel) 479 | l.AddHandler(h) 480 | SetDefaultLogger(l) 481 | 482 | b.ResetTimer() 483 | 484 | for i := 0; i < b.N; i++ { 485 | Infof("test") 486 | } 487 | l.Close() 488 | } 489 | 490 | func BenchmarkBufferedFileLoggerParallel(b *testing.B) { 491 | golog.StartFastTimer() 492 | defer golog.StopFastTimer() 493 | 494 | path := filepath.Join(os.TempDir(), "test.log") 495 | os.Remove(path) 496 | w, err := golog.NewBufferedFileWriter(path) 497 | if err != nil { 498 | b.Error(err) 499 | } 500 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 501 | h.AddWriter(w) 502 | l := golog.NewLogger(golog.InfoLevel) 503 | l.AddHandler(h) 504 | SetDefaultLogger(l) 505 | 506 | b.ResetTimer() 507 | 508 | b.RunParallel(func(pb *testing.PB) { 509 | for pb.Next() { 510 | Infof("test") 511 | } 512 | }) 513 | l.Close() 514 | } 515 | 516 | func BenchmarkConcurrentFileLogger(b *testing.B) { 517 | golog.StartFastTimer() 518 | defer golog.StopFastTimer() 519 | 520 | path := filepath.Join(os.TempDir(), "test.log") 521 | os.Remove(path) 522 | w, err := golog.NewConcurrentFileWriter(path, golog.BufferSize(1024*1024*8)) 523 | if err != nil { 524 | b.Error(err) 525 | } 526 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 527 | h.AddWriter(w) 528 | l := golog.NewLogger(golog.InfoLevel) 529 | l.AddHandler(h) 530 | SetDefaultLogger(l) 531 | 532 | b.ResetTimer() 533 | 534 | for i := 0; i < b.N; i++ { 535 | Infof("test") 536 | } 537 | l.Close() 538 | } 539 | 540 | func BenchmarkConcurrentFileLoggerParallel(b *testing.B) { 541 | golog.StartFastTimer() 542 | defer golog.StopFastTimer() 543 | 544 | path := filepath.Join(os.TempDir(), "test.log") 545 | os.Remove(path) 546 | w, err := golog.NewConcurrentFileWriter(path, golog.BufferSize(1024*1024*8)) 547 | if err != nil { 548 | b.Error(err) 549 | } 550 | h := golog.NewHandler(golog.InfoLevel, golog.DefaultFormatter) 551 | h.AddWriter(w) 552 | l := golog.NewLogger(golog.InfoLevel) 553 | l.AddHandler(h) 554 | SetDefaultLogger(l) 555 | 556 | b.ResetTimer() 557 | 558 | b.RunParallel(func(pb *testing.PB) { 559 | for pb.Next() { 560 | Infof("test") 561 | } 562 | }) 563 | l.Close() 564 | } 565 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package golog 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "sort" 14 | "sync" 15 | "time" 16 | ) 17 | 18 | const ( 19 | defaultBufferSize = 1024 * 1024 * 4 20 | 21 | fileFlag = os.O_WRONLY | os.O_CREATE | os.O_APPEND 22 | fileMode = 0644 23 | flushDuration = time.Millisecond * 100 24 | 25 | rotateByDateFormat = "-20060102.log" // -YYYYmmdd.log 26 | rotateByHourFormat = "-2006010215.log" // -YYYYmmddHH.log 27 | ) 28 | 29 | // RotateDuration specifies rotate duration type, should be either RotateByDate or RotateByHour. 30 | type RotateDuration uint8 31 | 32 | const ( 33 | // RotateByDate set the log file to be rotated each day. 34 | RotateByDate RotateDuration = iota 35 | // RotateByHour set the log file to be rotated each hour. 36 | RotateByHour 37 | ) 38 | 39 | // DiscardWriter is a WriteCloser which write everything to devNull 40 | type DiscardWriter struct { 41 | io.Writer 42 | } 43 | 44 | // NewDiscardWriter creates a new ConsoleWriter. 45 | func NewDiscardWriter() *DiscardWriter { 46 | return &DiscardWriter{Writer: ioutil.Discard} 47 | } 48 | 49 | // Close sets its Writer to nil. 50 | func (w *DiscardWriter) Close() error { 51 | w.Writer = nil 52 | return nil 53 | } 54 | 55 | // A ConsoleWriter is a writer which should not be actually closed. 56 | type ConsoleWriter struct { 57 | *os.File // faster than io.Writer 58 | } 59 | 60 | // NewConsoleWriter creates a new ConsoleWriter. 61 | func NewConsoleWriter(f *os.File) *ConsoleWriter { 62 | return &ConsoleWriter{File: f} 63 | } 64 | 65 | // NewStdoutWriter creates a new stdout writer. 66 | func NewStdoutWriter() *ConsoleWriter { 67 | return NewConsoleWriter(os.Stdout) 68 | } 69 | 70 | // NewStderrWriter creates a new stderr writer. 71 | func NewStderrWriter() *ConsoleWriter { 72 | return NewConsoleWriter(os.Stderr) 73 | } 74 | 75 | // Close sets its File to nil. 76 | func (w *ConsoleWriter) Close() error { 77 | w.File = nil 78 | return nil 79 | } 80 | 81 | // NewFileWriter creates a FileWriter by its path. 82 | func NewFileWriter(path string) (*os.File, error) { 83 | return os.OpenFile(path, fileFlag, fileMode) 84 | } 85 | 86 | type bufferedFileWriter struct { 87 | file *os.File 88 | buffer *bufio.Writer 89 | bufferSize uint32 90 | } 91 | 92 | type BufferedFileWriterOption func(*bufferedFileWriter) 93 | 94 | // BufferSize sets the buffer size. 95 | func BufferSize(size uint32) BufferedFileWriterOption { 96 | return func(w *bufferedFileWriter) { 97 | if size >= 1024 { 98 | w.bufferSize = size 99 | } 100 | } 101 | } 102 | 103 | // A BufferedFileWriter is a buffered file writer. 104 | // The written bytes will be flushed to the log file every 0.1 second, 105 | // or when reaching the buffer capacity (4 MB). 106 | type BufferedFileWriter struct { 107 | bufferedFileWriter 108 | lock sync.Mutex 109 | stopChan chan struct{} 110 | updateChan chan struct{} 111 | updated bool 112 | } 113 | 114 | // NewBufferedFileWriter creates a new BufferedFileWriter. 115 | func NewBufferedFileWriter(path string, options ...BufferedFileWriterOption) (*BufferedFileWriter, error) { 116 | f, err := os.OpenFile(path, fileFlag, fileMode) 117 | if err != nil { 118 | return nil, err 119 | } 120 | w := &BufferedFileWriter{ 121 | bufferedFileWriter: bufferedFileWriter{ 122 | file: f, 123 | bufferSize: defaultBufferSize, 124 | }, 125 | updateChan: make(chan struct{}, 1), 126 | stopChan: make(chan struct{}), 127 | } 128 | 129 | for _, option := range options { 130 | option(&w.bufferedFileWriter) 131 | } 132 | w.buffer = bufio.NewWriterSize(f, int(w.bufferSize)) 133 | 134 | go w.schedule() 135 | return w, nil 136 | } 137 | 138 | func (w *BufferedFileWriter) schedule() { 139 | timer := time.NewTimer(0) 140 | for { 141 | select { 142 | case <-w.updateChan: 143 | // something has been written to the buffer, it can be flushed to the file later 144 | stopTimer(timer) 145 | timer.Reset(flushDuration) 146 | case <-w.stopChan: 147 | stopTimer(timer) 148 | return 149 | } 150 | 151 | select { 152 | case <-timer.C: 153 | var err error 154 | w.lock.Lock() 155 | if w.file != nil { // not closed 156 | w.updated = false 157 | err = w.buffer.Flush() 158 | } 159 | w.lock.Unlock() 160 | if err != nil { 161 | logError(err) 162 | } 163 | case <-w.stopChan: 164 | stopTimer(timer) 165 | return 166 | } 167 | } 168 | } 169 | 170 | // Write writes a byte slice to the buffer. 171 | func (w *BufferedFileWriter) Write(p []byte) (n int, err error) { 172 | w.lock.Lock() 173 | n, err = w.buffer.Write(p) 174 | if !w.updated && n > 0 && w.buffer.Buffered() > 0 { // checks w.updated to prevent notifying w.updateChan twice 175 | w.updated = true 176 | w.lock.Unlock() 177 | 178 | select { // ignores if blocked 179 | case w.updateChan <- struct{}{}: 180 | default: 181 | } 182 | } else { 183 | w.lock.Unlock() 184 | } 185 | return 186 | } 187 | 188 | // Close flushes the buffer, then closes the file writer. 189 | func (w *BufferedFileWriter) Close() error { 190 | close(w.stopChan) 191 | w.lock.Lock() 192 | err := w.buffer.Flush() 193 | w.buffer = nil 194 | if err == nil { 195 | err = w.file.Close() 196 | } else { 197 | e := w.file.Close() 198 | if e != nil { 199 | logError(e) 200 | } 201 | } 202 | w.file = nil 203 | w.lock.Unlock() 204 | return err 205 | } 206 | 207 | // A RotatingFileWriter is a buffered file writer which will rotate before reaching its maxSize. 208 | // An exception is when a record is larger than maxSize, it won't be separated into 2 files. 209 | // It keeps at most backupCount backups. 210 | type RotatingFileWriter struct { 211 | BufferedFileWriter 212 | path string 213 | pos uint64 214 | maxSize uint64 215 | backupCount uint8 216 | } 217 | 218 | // NewRotatingFileWriter creates a new RotatingFileWriter. 219 | func NewRotatingFileWriter(path string, maxSize uint64, backupCount uint8, options ...BufferedFileWriterOption) (*RotatingFileWriter, error) { 220 | if maxSize == 0 { 221 | return nil, errors.New("maxSize cannot be 0") 222 | } 223 | 224 | if backupCount == 0 { 225 | return nil, errors.New("backupCount cannot be 0") 226 | } 227 | 228 | f, err := os.OpenFile(path, fileFlag, fileMode) 229 | if err != nil { 230 | return nil, err 231 | } 232 | 233 | stat, err := f.Stat() 234 | if err != nil { 235 | e := f.Close() 236 | if e != nil { 237 | logError(e) 238 | } 239 | return nil, err 240 | } 241 | 242 | w := RotatingFileWriter{ 243 | BufferedFileWriter: BufferedFileWriter{ 244 | bufferedFileWriter: bufferedFileWriter{ 245 | file: f, 246 | bufferSize: defaultBufferSize, 247 | }, 248 | updateChan: make(chan struct{}, 1), 249 | stopChan: make(chan struct{}), 250 | }, 251 | path: path, 252 | pos: uint64(stat.Size()), 253 | maxSize: maxSize, 254 | backupCount: backupCount, 255 | } 256 | 257 | for _, option := range options { 258 | option(&w.bufferedFileWriter) 259 | } 260 | w.buffer = bufio.NewWriterSize(f, int(w.bufferSize)) 261 | 262 | go w.schedule() 263 | return &w, nil 264 | } 265 | 266 | // Write writes a byte slice to the buffer and rotates if reaching its maxSize. 267 | func (w *RotatingFileWriter) Write(p []byte) (n int, err error) { 268 | w.lock.Lock() 269 | defer w.lock.Unlock() 270 | 271 | n, err = w.buffer.Write(p) 272 | if n > 0 { 273 | w.pos += uint64(n) 274 | 275 | if w.pos >= w.maxSize { 276 | e := w.rotate() 277 | if e != nil { 278 | logError(e) 279 | if err == nil { // don't shadow Write() error 280 | err = e 281 | } 282 | } 283 | return // w.rotate() also calls w.buffer.Flush(), no need to notify w.updateChan 284 | } 285 | 286 | if !w.updated && w.buffer.Buffered() > 0 { 287 | w.updated = true 288 | 289 | select { // ignores if blocked 290 | case w.updateChan <- struct{}{}: 291 | default: 292 | } 293 | } 294 | } 295 | 296 | return 297 | } 298 | 299 | // rotate rotates the log file. It should be called within a lock block. 300 | func (w *RotatingFileWriter) rotate() error { 301 | if w.file == nil { // was closed 302 | return os.ErrClosed 303 | } 304 | 305 | err := w.buffer.Flush() 306 | if err != nil { 307 | return err 308 | } 309 | 310 | err = w.file.Close() 311 | w.pos = 0 312 | if err != nil { 313 | w.file = nil 314 | w.buffer = nil 315 | return err 316 | } 317 | 318 | for i := w.backupCount; i > 1; i-- { 319 | oldPath := fmt.Sprintf("%s.%d", w.path, i-1) 320 | newPath := fmt.Sprintf("%s.%d", w.path, i) 321 | e := os.Rename(oldPath, newPath) 322 | if e != nil { 323 | logError(e) 324 | } 325 | } 326 | 327 | err = os.Rename(w.path, w.path+".1") 328 | if err != nil { 329 | w.file = nil 330 | w.buffer = nil 331 | return err 332 | } 333 | 334 | f, err := os.OpenFile(w.path, fileFlag, fileMode) 335 | if err != nil { 336 | w.file = nil 337 | w.buffer = nil 338 | return err 339 | } 340 | 341 | w.file = f 342 | w.buffer.Reset(f) 343 | return nil 344 | } 345 | 346 | // A TimedRotatingFileWriter is a buffered file writer which will rotate by time. 347 | // Its rotateDuration can be either RotateByDate or RotateByHour. 348 | // It keeps at most backupCount backups. 349 | type TimedRotatingFileWriter struct { 350 | BufferedFileWriter 351 | pathPrefix string 352 | rotateDuration RotateDuration 353 | backupCount uint8 354 | } 355 | 356 | // NewTimedRotatingFileWriter creates a new TimedRotatingFileWriter. 357 | func NewTimedRotatingFileWriter(pathPrefix string, rotateDuration RotateDuration, backupCount uint8, options ...BufferedFileWriterOption) (*TimedRotatingFileWriter, error) { 358 | if backupCount == 0 { 359 | return nil, errors.New("backupCount cannot be 0") 360 | } 361 | 362 | f, err := openTimedRotatingFile(pathPrefix, rotateDuration) 363 | if err != nil { 364 | return nil, err 365 | } 366 | 367 | w := TimedRotatingFileWriter{ 368 | BufferedFileWriter: BufferedFileWriter{ 369 | bufferedFileWriter: bufferedFileWriter{ 370 | file: f, 371 | bufferSize: defaultBufferSize, 372 | }, 373 | updateChan: make(chan struct{}, 1), 374 | stopChan: make(chan struct{}), 375 | }, 376 | pathPrefix: pathPrefix, 377 | rotateDuration: rotateDuration, 378 | backupCount: backupCount, 379 | } 380 | 381 | for _, option := range options { 382 | option(&w.bufferedFileWriter) 383 | } 384 | w.buffer = bufio.NewWriterSize(f, int(w.bufferSize)) 385 | 386 | go w.schedule() 387 | return &w, nil 388 | } 389 | 390 | func (w *TimedRotatingFileWriter) schedule() { 391 | lock := &w.lock 392 | flushTimer := time.NewTimer(0) 393 | duration := nextRotateDuration(w.rotateDuration) 394 | rotateTimer := time.NewTimer(duration) 395 | 396 | for { 397 | updateLoop: 398 | for { 399 | select { 400 | case <-w.updateChan: 401 | stopTimer(flushTimer) 402 | flushTimer.Reset(flushDuration) 403 | break updateLoop 404 | case <-rotateTimer.C: 405 | err := w.rotate(rotateTimer) 406 | if err != nil { 407 | logError(err) 408 | } 409 | case <-w.stopChan: 410 | stopTimer(flushTimer) 411 | stopTimer(rotateTimer) 412 | return 413 | } 414 | } 415 | 416 | flushLoop: 417 | for { 418 | select { 419 | case <-flushTimer.C: 420 | lock.Lock() 421 | var err error 422 | if w.file != nil { // not closed 423 | w.updated = false 424 | err = w.buffer.Flush() 425 | } 426 | lock.Unlock() 427 | if err != nil { 428 | logError(err) 429 | } 430 | break flushLoop 431 | case <-rotateTimer.C: 432 | err := w.rotate(rotateTimer) 433 | if err != nil { 434 | logError(err) 435 | } 436 | case <-w.stopChan: 437 | stopTimer(flushTimer) 438 | stopTimer(rotateTimer) 439 | return 440 | } 441 | } 442 | } 443 | } 444 | 445 | // rotate rotates the log file. 446 | func (w *TimedRotatingFileWriter) rotate(timer *time.Timer) error { 447 | w.lock.Lock() 448 | if w.file == nil { // was closed 449 | w.lock.Unlock() 450 | return nil // usually happens when program exits, should be ignored 451 | } 452 | 453 | err := w.buffer.Flush() 454 | if err != nil { 455 | w.lock.Unlock() 456 | return err 457 | } 458 | 459 | err = w.file.Close() 460 | if err != nil { 461 | w.lock.Unlock() 462 | return err 463 | } 464 | 465 | f, err := openTimedRotatingFile(w.pathPrefix, w.rotateDuration) 466 | if err != nil { 467 | w.buffer = nil 468 | w.file = nil 469 | w.lock.Unlock() 470 | return err 471 | } 472 | 473 | w.file = f 474 | w.buffer.Reset(f) 475 | 476 | duration := nextRotateDuration(w.rotateDuration) 477 | timer.Reset(duration) 478 | w.lock.Unlock() 479 | 480 | go w.purge() 481 | return nil 482 | } 483 | 484 | // purge removes the outdated backups. 485 | func (w *TimedRotatingFileWriter) purge() { 486 | pathes, err := filepath.Glob(w.pathPrefix + "*") 487 | if err != nil { 488 | logError(err) 489 | return 490 | } 491 | 492 | count := len(pathes) - int(w.backupCount) - 1 493 | if count > 0 { 494 | var name string 495 | w.lock.Lock() 496 | if w.file != nil { // not closed 497 | name = w.file.Name() 498 | } 499 | w.lock.Unlock() 500 | sort.Strings(pathes) 501 | for i := 0; i < count; i++ { 502 | path := pathes[i] 503 | if path != name { 504 | err = os.Remove(path) 505 | if err != nil { 506 | logError(err) 507 | } 508 | } 509 | } 510 | } 511 | } 512 | 513 | // openTimedRotatingFile opens a log file for TimedRotatingFileWriter 514 | func openTimedRotatingFile(path string, rotateDuration RotateDuration) (*os.File, error) { 515 | var pathSuffix string 516 | t := now() 517 | switch rotateDuration { 518 | case RotateByDate: 519 | pathSuffix = t.Format(rotateByDateFormat) 520 | case RotateByHour: 521 | pathSuffix = t.Format(rotateByHourFormat) 522 | default: 523 | return nil, errors.New("invalid rotateDuration") 524 | } 525 | 526 | return os.OpenFile(path+pathSuffix, fileFlag, fileMode) 527 | } 528 | 529 | // nextRotateDuration returns the next rotate duration for the rotateTimer. 530 | // It is defined as a variable in order to mock it in the unit testing. 531 | var nextRotateDuration = func(rotateDuration RotateDuration) time.Duration { 532 | now := now() 533 | var nextTime time.Time 534 | if rotateDuration == RotateByDate { 535 | nextTime = time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location()) 536 | } else { 537 | nextTime = time.Date(now.Year(), now.Month(), now.Day(), now.Hour()+1, 0, 0, 0, now.Location()) 538 | } 539 | return nextTime.Sub(now) 540 | } 541 | 542 | type ConcurrentFileWriter struct { 543 | bufferedFileWriter 544 | cpuCount int 545 | locks []sync.Mutex 546 | buffers []*bytes.Buffer 547 | stopChan chan struct{} 548 | stoppedChan chan struct{} 549 | } 550 | 551 | // NewBufferedFileWriter creates a new BufferedFileWriter. 552 | func NewConcurrentFileWriter(path string, options ...BufferedFileWriterOption) (*ConcurrentFileWriter, error) { 553 | f, err := os.OpenFile(path, fileFlag, fileMode) 554 | if err != nil { 555 | return nil, err 556 | } 557 | 558 | cpuCount := runtime.GOMAXPROCS(0) 559 | 560 | w := &ConcurrentFileWriter{ 561 | bufferedFileWriter: bufferedFileWriter{ 562 | file: f, 563 | bufferSize: defaultBufferSize, 564 | }, 565 | cpuCount: cpuCount, 566 | locks: make([]sync.Mutex, cpuCount), 567 | buffers: make([]*bytes.Buffer, cpuCount), 568 | stopChan: make(chan struct{}), 569 | stoppedChan: make(chan struct{}, 1), 570 | } 571 | 572 | for _, option := range options { 573 | option(&w.bufferedFileWriter) 574 | } 575 | 576 | w.buffer = bufio.NewWriterSize(f, int(w.bufferSize)) 577 | for i := 0; i < cpuCount; i++ { 578 | w.buffers[i] = bytes.NewBuffer(make([]byte, 0, w.bufferSize)) 579 | } 580 | 581 | go w.schedule() 582 | return w, nil 583 | } 584 | 585 | func (w *ConcurrentFileWriter) schedule() { 586 | timer := time.NewTimer(flushDuration) 587 | for { 588 | select { 589 | case <-timer.C: 590 | for shard := 0; shard < w.cpuCount; shard++ { 591 | w.locks[shard].Lock() 592 | buffer := w.buffers[shard] 593 | if buffer.Len() > 0 { 594 | w.buffer.Write(buffer.Bytes()) 595 | buffer.Reset() 596 | } 597 | w.locks[shard].Unlock() 598 | } 599 | 600 | if w.buffer.Buffered() > 0 { 601 | err := w.buffer.Flush() 602 | if err != nil { 603 | logError(err) 604 | } 605 | } 606 | 607 | timer.Reset(flushDuration) 608 | case <-w.stopChan: 609 | stopTimer(timer) 610 | w.stoppedChan <- struct{}{} 611 | return 612 | } 613 | } 614 | } 615 | 616 | // Write writes a byte slice to the buffer. 617 | func (w *ConcurrentFileWriter) Write(p []byte) (n int, err error) { 618 | shard := runtime_procPin() 619 | runtime_procUnpin() // can't hold the lock for long 620 | 621 | w.locks[shard].Lock() 622 | n, err = w.buffers[shard].Write(p) 623 | w.locks[shard].Unlock() 624 | return 625 | } 626 | 627 | // Close flushes the buffer, then closes the file writer. 628 | func (w *ConcurrentFileWriter) Close() (err error) { 629 | close(w.stopChan) // stops schedule() 630 | <-w.stoppedChan // waits for schedule() to finish, so the rest code can run without locks 631 | 632 | for shard := 0; shard < w.cpuCount; shard++ { 633 | buffer := w.buffers[shard] 634 | if buffer.Len() > 0 { 635 | w.buffer.Write(buffer.Bytes()) 636 | buffer.Reset() 637 | } 638 | } 639 | 640 | if w.buffer.Buffered() > 0 { 641 | err = w.buffer.Flush() 642 | } 643 | if err == nil { 644 | err = w.file.Close() 645 | } else { 646 | e := w.file.Close() 647 | if e != nil { 648 | logError(e) 649 | } 650 | } 651 | w.file = nil 652 | return err 653 | } 654 | --------------------------------------------------------------------------------