├── .gitignore ├── Gododir └── main.go ├── LICENSE ├── README.md ├── images └── demo.gif └── v1 ├── bench └── bench_test.go ├── callstack.go ├── cmd ├── demo │ ├── main.ansi │ └── main.go ├── filter │ ├── README.md │ └── main.go └── reldir │ ├── README.md │ └── foo.go ├── concurrentWriter.go ├── defaultLogger.go ├── env.go ├── formatter.go ├── happyDevFormatter.go ├── init.go ├── init_test.go ├── jsonFormatter.go ├── logger.go ├── logger_test.go ├── methods.go ├── nullLogger.go ├── pool.go ├── textFormatter.go ├── util.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | godobin* 2 | v1/cmd/demo/demo 3 | v1/cmd/filter/filter 4 | *.exe 5 | -------------------------------------------------------------------------------- /Gododir/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/mattn/go-colorable" 11 | "github.com/mgutz/ansi" 12 | do "gopkg.in/godo.v2" 13 | ) 14 | 15 | type pair struct { 16 | description string 17 | command string 18 | } 19 | 20 | var stdout io.Writer 21 | 22 | var promptColor = ansi.ColorCode("cyan+h") 23 | var commentColor = ansi.ColorCode("yellow+h") 24 | var titleColor = ansi.ColorCode("green+h") 25 | var subtitleColor = ansi.ColorCode("black+h") 26 | var normal = ansi.DefaultFG 27 | var wd string 28 | 29 | func init() { 30 | wd, _ = os.Getwd() 31 | stdout = colorable.NewColorableStdout() 32 | } 33 | 34 | func clear() { 35 | do.Bash("clear") 36 | // leave a single line at top so the window 37 | // overlay doesn't have to be exact 38 | fmt.Fprintln(stdout, "") 39 | } 40 | 41 | func pseudoType(s string, color string) { 42 | if color != "" { 43 | fmt.Fprint(stdout, color) 44 | } 45 | for _, r := range s { 46 | fmt.Fprint(stdout, string(r)) 47 | time.Sleep(50 * time.Millisecond) 48 | } 49 | if color != "" { 50 | fmt.Fprint(stdout, ansi.Reset) 51 | } 52 | } 53 | 54 | func pseudoTypeln(s string, color string) { 55 | pseudoType(s, color) 56 | fmt.Fprint(stdout, "\n") 57 | } 58 | 59 | func pseudoPrompt(prompt, s string) { 60 | pseudoType(prompt, promptColor) 61 | //fmt.Fprint(stdout, promptFn(prompt)) 62 | pseudoType(s, normal) 63 | } 64 | 65 | func intro(title, subtitle string, delay time.Duration) { 66 | clear() 67 | pseudoType("\n\n\t"+title+"\n\n", titleColor) 68 | pseudoType("\t"+subtitle, subtitleColor) 69 | time.Sleep(delay) 70 | } 71 | 72 | func typeCommand(description, commandStr string) { 73 | clear() 74 | pseudoTypeln("# "+description, commentColor) 75 | pseudoType("> ", promptColor) 76 | pseudoType(commandStr, normal) 77 | time.Sleep(200 * time.Millisecond) 78 | fmt.Fprintln(stdout, "") 79 | } 80 | 81 | var version = "v1" 82 | 83 | func relv(p string) string { 84 | return filepath.Join(version, p) 85 | } 86 | func absv(p string) string { 87 | return filepath.Join(wd, version, p) 88 | } 89 | 90 | func tasks(p *do.Project) { 91 | p.Task("bench", nil, func(c *do.Context) { 92 | c.Run("LOGXI=* go test -bench . -benchmem", do.M{"$in": "v1/bench"}) 93 | }) 94 | 95 | p.Task("build", nil, func(c *do.Context) { 96 | c.Run("go build", do.M{"$in": "v1/cmd/demo"}) 97 | }) 98 | 99 | p.Task("linux-build", nil, func(c *do.Context) { 100 | c.Bash(` 101 | set -e 102 | GOOS=linux GOARCH=amd64 go build 103 | scp -F ~/projects/provision/matcherino/ssh.vagrant.config demo devmaster1:~/. 104 | `, do.M{"$in": "v1/cmd/demo"}) 105 | }) 106 | 107 | p.Task("etcd-set", nil, func(c *do.Context) { 108 | kv := c.Args.NonFlags() 109 | if len(kv) != 2 { 110 | do.Halt(fmt.Errorf("godo etcd-set -- KEY VALUE")) 111 | } 112 | 113 | c.Run( 114 | `curl -L http://127.0.0.1:4001/v2/keys/{{.key}} -XPUT -d value="{{.value}}"`, 115 | do.M{"key": kv[0], "value": kv[1]}, 116 | ) 117 | }) 118 | 119 | p.Task("etcd-del", nil, func(c *do.Context) { 120 | kv := c.Args.Leftover() 121 | if len(kv) != 1 { 122 | do.Halt(fmt.Errorf("godo etcd-del -- KEY")) 123 | } 124 | c.Run( 125 | `curl -L http://127.0.0.1:4001/v2/keys/{{.key}} -XDELETE`, 126 | do.M{"key": kv[0]}, 127 | ) 128 | }) 129 | 130 | p.Task("demo", nil, func(c *do.Context) { 131 | c.Run("go run main.go", do.M{"$in": "v1/cmd/demo"}) 132 | }) 133 | 134 | p.Task("demo2", nil, func(c *do.Context) { 135 | c.Run("go run main.go", do.M{"$in": "v1/cmd/demo2"}) 136 | }) 137 | 138 | p.Task("filter", do.S{"build"}, func(c *do.Context) { 139 | c.Run("go build", do.M{"$in": "v1/cmd/filter"}) 140 | c.Bash("LOGXI=* ../demo/demo | ./filter", do.M{"$in": "v1/cmd/filter"}) 141 | }) 142 | 143 | p.Task("gifcast", do.S{"build"}, func(*do.Context) { 144 | commands := []pair{ 145 | { 146 | `create a simple app demo`, 147 | `cat main.ansi`, 148 | }, 149 | { 150 | `running demo displays only warnings and errors with context`, 151 | `demo`, 152 | }, 153 | { 154 | `show all log levels`, 155 | `LOGXI=* demo`, 156 | }, 157 | { 158 | `enable/disable loggers with level`, 159 | `LOGXI=*=ERR,models demo`, 160 | }, 161 | { 162 | `create custom 256 colors colorscheme, pink==200`, 163 | `LOGXI_COLORS=*=black+h,ERR=200+b,key=blue+h demo`, 164 | }, 165 | { 166 | `put keys on newline, set time format, less context`, 167 | `LOGXI=* LOGXI_FORMAT=pretty,maxcol=80,t=04:05.000,context=0 demo`, 168 | }, 169 | { 170 | `logxi defaults to fast, unadorned JSON in production`, 171 | `demo | cat`, 172 | }, 173 | } 174 | 175 | // setup time for ecorder, user presses enter when ready 176 | clear() 177 | do.Prompt("") 178 | 179 | intro( 180 | "log XI", 181 | "structured. faster. friendlier.\n\n\n\n\t::mgutz", 182 | 1*time.Second, 183 | ) 184 | 185 | for _, cmd := range commands { 186 | typeCommand(cmd.description, cmd.command) 187 | do.Bash(cmd.command, do.M{"$in": "v1/cmd/demo"}) 188 | time.Sleep(3500 * time.Millisecond) 189 | } 190 | 191 | clear() 192 | do.Prompt("") 193 | }) 194 | 195 | p.Task("demo-gif", nil, func(c *do.Context) { 196 | c.Bash(`cp ~/Desktop/demo.gif images`) 197 | }) 198 | 199 | p.Task("bench-allocs", nil, func(c *do.Context) { 200 | c.Bash(`go test -bench . -benchmem -run=none | grep "allocs\|^Bench"`, do.M{"$in": "v1/bench"}) 201 | }).Description("Runs benchmarks with allocs") 202 | 203 | p.Task("benchjson", nil, func(c *do.Context) { 204 | c.Bash("go test -bench=BenchmarkLoggerJSON -benchmem", do.M{"$in": "v1/bench"}) 205 | }) 206 | 207 | p.Task("test", nil, func(c *do.Context) { 208 | c.Run("LOGXI=* go test", do.M{"$in": "v1"}) 209 | //Run("LOGXI=* go test -run=TestColors", M{"$in": "v1"}) 210 | }) 211 | 212 | p.Task("isolate", do.S{"build"}, func(c *do.Context) { 213 | c.Bash("LOGXI=* LOGXI_FORMAT=fit,maxcol=80,t=04:05.000,context=2 demo", do.M{"$in": "v1/cmd/demo"}) 214 | }) 215 | 216 | p.Task("install", nil, func(c *do.Context) { 217 | packages := []string{ 218 | "github.com/mattn/go-colorable", 219 | "github.com/mattn/go-isatty", 220 | "github.com/mgutz/ansi", 221 | "github.com/stretchr/testify/assert", 222 | 223 | // needed for benchmarks in bench/ 224 | "github.com/Sirupsen/logrus", 225 | "gopkg.in/inconshreveable/log15.v2", 226 | } 227 | for _, pkg := range packages { 228 | c.Run("go get -u " + pkg) 229 | } 230 | }).Description("Installs dependencies") 231 | 232 | } 233 | 234 | func main() { 235 | do.Godo(tasks) 236 | } 237 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Mario Gutierrez 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![demo](https://github.com/mgutz/logxi/raw/master/images/demo.gif) 3 | 4 | # logxi 5 | 6 | log XI is a structured [12-factor app](http://12factor.net/logs) 7 | logger built for speed and happy development. 8 | 9 | * Simpler. Sane no-configuration defaults out of the box. 10 | * Faster. See benchmarks vs logrus and log15. 11 | * Structured. Key-value pairs are enforced. Logs JSON in production. 12 | * Configurable. Enable/disalbe Loggers and levels via env vars. 13 | * Friendlier. Happy, colorful and developer friendly logger in terminal. 14 | * Helpul. Traces, warnings and errors are emphasized with file, line 15 | number and callstack. 16 | * Efficient. Has level guards to avoid cost of building complex arguments. 17 | 18 | 19 | ### Requirements 20 | 21 | Go 1.3+ 22 | 23 | ### Installation 24 | 25 | go get -u github.com/mgutz/logxi/v1 26 | 27 | ### Getting Started 28 | 29 | ```go 30 | import "github.com/mgutz/logxi/v1" 31 | 32 | // create package variable for Logger interface 33 | var logger log.Logger 34 | 35 | func main() { 36 | // use default logger 37 | who := "mario" 38 | log.Info("Hello", "who", who) 39 | 40 | // create a logger with a unique identifier which 41 | // can be enabled from environment variables 42 | logger = log.New("pkg") 43 | 44 | // specify a writer, use NewConcurrentWriter if it is not concurrent 45 | // safe 46 | modelLogger = log.NewLogger(log.NewConcurrentWriter(os.Stdout), "models") 47 | 48 | db, err := sql.Open("postgres", "dbname=testdb") 49 | if err != nil { 50 | modelLogger.Error("Could not open database", "err", err) 51 | } 52 | 53 | fruit := "apple" 54 | languages := []string{"go", "javascript"} 55 | if log.IsDebug() { 56 | // use key-value pairs after message 57 | logger.Debug("OK", "fruit", fruit, "languages", languages) 58 | } 59 | } 60 | ``` 61 | 62 | logxi defaults to showing warnings and above. To view all logs 63 | 64 | LOGXI=* go run main.go 65 | 66 | ## Highlights 67 | 68 | This logger package 69 | 70 | * Is fast in production environment 71 | 72 | A logger should be efficient and minimize performance tax. 73 | logxi encodes JSON 2X faster than logrus and log15 with primitive types. 74 | When diagnosing a problem in production, troubleshooting often means 75 | enabling small trace data in `Debug` and `Info` statements for some 76 | period of time. 77 | 78 | # primitive types 79 | BenchmarkLogxi 100000 20021 ns/op 2477 B/op 66 allocs/op 80 | BenchmarkLogrus 30000 46372 ns/op 8991 B/op 196 allocs/op 81 | BenchmarkLog15 20000 62974 ns/op 9244 B/op 236 allocs/op 82 | 83 | # nested object 84 | BenchmarkLogxiComplex 30000 44448 ns/op 6416 B/op 190 allocs/op 85 | BenchmarkLogrusComplex 20000 65006 ns/op 12231 B/op 278 allocs/op 86 | BenchmarkLog15Complex 20000 92880 ns/op 13172 B/op 311 allocs/op 87 | 88 | * Is developer friendly in the terminal. The HappyDevFormatter 89 | is colorful, prints file and line numbers for traces, warnings 90 | and errors. Arguments are printed in the order they are coded. 91 | Errors print the call stack. 92 | 93 | `HappyDevFormatter` is not too concerned with performance 94 | and delegates to JSONFormatter internally. 95 | 96 | * Logs machine parsable output in production environments. 97 | The default formatter for non terminals is `JSONFormatter`. 98 | 99 | `TextFormatter` may also be used which is MUCH faster than 100 | JSON but there is no guarantee it can be easily parsed. 101 | 102 | * Has level guards to avoid the cost of building arguments. Get in the 103 | habit of using guards. 104 | 105 | if log.IsDebug() { 106 | log.Debug("some ", "key1", expensive()) 107 | } 108 | 109 | * Conforms to a logging interface so it can be replaced. 110 | 111 | type Logger interface { 112 | Trace(msg string, args ...interface{}) 113 | Debug(msg string, args ...interface{}) 114 | Info(msg string, args ...interface{}) 115 | Warn(msg string, args ...interface{}) error 116 | Error(msg string, args ...interface{}) error 117 | Fatal(msg string, args ...interface{}) 118 | Log(level int, msg string, args []interface{}) 119 | 120 | SetLevel(int) 121 | IsTrace() bool 122 | IsDebug() bool 123 | IsInfo() bool 124 | IsWarn() bool 125 | // Error, Fatal not needed, those SHOULD always be logged 126 | } 127 | 128 | * Standardizes on key-value pair argument sequence 129 | 130 | ```go 131 | log.Debug("inside Fn()", "key1", value1, "key2", value2) 132 | 133 | // instead of this 134 | log.WithFields(logrus.Fields{"m": "pkg", "key1": value1, "key2": value2}).Debug("inside fn()") 135 | ``` 136 | logxi logs `FIX_IMBALANCED_PAIRS =>` if key-value pairs are imbalanced 137 | 138 | `log.Warn and log.Error` are special cases and return error: 139 | 140 | ```go 141 | return log.Error(msg) //=> fmt.Errorf(msg) 142 | return log.Error(msg, "err", err) //=> err 143 | ``` 144 | 145 | * Supports Color Schemes (256 colors) 146 | 147 | `log.New` creates a logger that supports color schemes 148 | 149 | logger := log.New("mylog") 150 | 151 | To customize scheme 152 | 153 | # emphasize errors with white text on red background 154 | LOGXI_COLORS="ERR=white:red" yourapp 155 | 156 | # emphasize errors with pink = 200 on 256 colors table 157 | LOGXI_COLORS="ERR=200" yourapp 158 | 159 | * Is suppressable in unit tests 160 | 161 | ```go 162 | func TestErrNotFound() { 163 | log.Suppress(true) 164 | defer log.Suppress(false) 165 | ... 166 | } 167 | ``` 168 | 169 | 170 | 171 | ## Configuration 172 | 173 | ### Enabling/Disabling Loggers 174 | 175 | By default logxi logs entries whose level is `LevelWarn` or above when 176 | using a terminal. For non-terminals, entries with level `LevelError` and 177 | above are logged. 178 | 179 | To quickly see all entries use short form 180 | 181 | # enable all, disable log named foo 182 | LOGXI=*,-foo yourapp 183 | 184 | To better control logs in production, use long form which allows 185 | for granular control of levels 186 | 187 | # the above statement is equivalent to this 188 | LOGXI=*=DBG,foo=OFF yourapp 189 | 190 | `DBG` should obviously not be used in production unless for 191 | troubleshooting. See `LevelAtoi` in `logger.go` for values. 192 | For example, there is a problem in the data access layer 193 | in production. 194 | 195 | # Set all to Error and set data related packages to Debug 196 | LOGXI=*=ERR,models=DBG,dat*=DBG,api=DBG yourapp 197 | 198 | ### Format 199 | 200 | The format may be set via `LOGXI_FORMAT` environment 201 | variable. Valid values are `"happy", "text", "JSON", "LTSV"` 202 | 203 | # Use JSON in production with custom time 204 | LOGXI_FORMAT=JSON,t=2006-01-02T15:04:05.000000-0700 yourapp 205 | 206 | The "happy" formatter has more options 207 | 208 | * pretty - puts each key-value pair indented on its own line 209 | 210 | "happy" default to fitting key-value pair onto the same line. If 211 | result characters are longer than `maxcol` then the pair will be 212 | put on the next line and indented 213 | 214 | * maxcol - maximum number of columns before forcing a key to be on its 215 | own line. If you want everything on a single line, set this to high 216 | value like 1000. Default is 80. 217 | 218 | * context - the number of context lines to print on source. Set to -1 219 | to see only file:lineno. Default is 2. 220 | 221 | 222 | ### Color Schemes 223 | 224 | The color scheme may be set with `LOGXI_COLORS` environment variable. For 225 | example, the default dark scheme is emulated like this 226 | 227 | # on non-Windows, see Windows support below 228 | export LOGXI_COLORS=key=cyan+h,value,misc=blue+h,source=magenta,TRC,DBG,WRN=yellow,INF=green,ERR=red+h 229 | yourapp 230 | 231 | # color only errors 232 | LOGXI_COLORS=ERR=red yourapp 233 | 234 | See [ansi](http://github.com/mgutz/ansi) package for styling. An empty 235 | value, like "value" and "DBG" above means use default foreground and 236 | background on terminal. 237 | 238 | Keys 239 | 240 | * \* - default color 241 | * TRC - trace color 242 | * DBG - debug color 243 | * WRN - warn color 244 | * INF - info color 245 | * ERR - error color 246 | * message - message color 247 | * key - key color 248 | * value - value color unless WRN or ERR 249 | * misc - time and log name color 250 | * source - source context color (excluding error line) 251 | 252 | #### Windows 253 | 254 | Use [ConEmu-Maximus5](https://github.com/Maximus5/ConEmu). 255 | Read this page about [256 colors](https://code.google.com/p/conemu-maximus5/wiki/Xterm256Colors). 256 | 257 | Colors in PowerShell and Command Prompt _work_ but not very pretty. 258 | 259 | ## Extending 260 | 261 | What about hooks? There are least two ways to do this 262 | 263 | * Implement your own `io.Writer` to write to external services. Be sure to set 264 | the formatter to JSON to faciliate decoding with Go's built-in streaming 265 | decoder. 266 | * Create an external filter. See `v1/cmd/filter` as an example. 267 | 268 | What about log rotation? 12 factor apps only concern themselves with 269 | STDOUT. Use shell redirection operators to write to a file. 270 | 271 | There are many utilities to rotate logs which accept STDIN as input. They can 272 | do many things like send alerts, etc. The two obvious choices are Apache's `rotatelogs` 273 | utility and `lograte`. 274 | 275 | ```sh 276 | yourapp | rotatelogs yourapp 86400 277 | ``` 278 | 279 | ## Testing 280 | 281 | ``` 282 | # install godo task runner 283 | go get -u gopkg.in/godo.v2/cmd/godo 284 | 285 | # install dependencies 286 | godo install -v 287 | 288 | # run test 289 | godo test 290 | 291 | # run bench with allocs (requires manual cleanup of output) 292 | godo bench-allocs 293 | ``` 294 | 295 | ## License 296 | 297 | MIT License 298 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgutz/logxi/aebf8a7d67ab4625e0fd4a665766fef9a709161b/images/demo.gif -------------------------------------------------------------------------------- /v1/bench/bench_test.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "encoding/json" 5 | L "log" 6 | "os" 7 | "testing" 8 | 9 | "github.com/Sirupsen/logrus" 10 | "github.com/mgutz/logxi/v1" 11 | "gopkg.in/inconshreveable/log15.v2" 12 | ) 13 | 14 | type M map[string]interface{} 15 | 16 | var testObject = M{ 17 | "foo": "bar", 18 | "bah": M{ 19 | "int": 1, 20 | "float": -100.23, 21 | "date": "06-01-01T15:04:05-0700", 22 | "bool": true, 23 | "nullable": nil, 24 | }, 25 | } 26 | 27 | var pid = os.Getpid() 28 | 29 | func toJSON(m map[string]interface{}) string { 30 | b, _ := json.Marshal(m) 31 | return string(b) 32 | } 33 | 34 | // These tests write out all log levels with concurrency turned on and 35 | // equivalent fields. 36 | 37 | func BenchmarkLog(b *testing.B) { 38 | //fmt.Println("") 39 | l := L.New(os.Stdout, "bench ", L.LstdFlags) 40 | b.ResetTimer() 41 | for i := 0; i < b.N; i++ { 42 | debug := map[string]interface{}{"l": "debug", "key1": 1, "key2": "string", "key3": false} 43 | l.Printf(toJSON(debug)) 44 | 45 | info := map[string]interface{}{"l": "info", "key1": 1, "key2": "string", "key3": false} 46 | l.Printf(toJSON(info)) 47 | 48 | warn := map[string]interface{}{"l": "warn", "key1": 1, "key2": "string", "key3": false} 49 | l.Printf(toJSON(warn)) 50 | 51 | err := map[string]interface{}{"l": "error", "key1": 1, "key2": "string", "key3": false} 52 | l.Printf(toJSON(err)) 53 | } 54 | b.StopTimer() 55 | } 56 | 57 | func BenchmarkLogComplex(b *testing.B) { 58 | //fmt.Println("") 59 | l := L.New(os.Stdout, "bench ", L.LstdFlags) 60 | b.ResetTimer() 61 | for i := 0; i < b.N; i++ { 62 | debug := map[string]interface{}{"l": "debug", "key1": 1, "obj": testObject} 63 | l.Printf(toJSON(debug)) 64 | 65 | info := map[string]interface{}{"l": "info", "key1": 1, "obj": testObject} 66 | l.Printf(toJSON(info)) 67 | 68 | warn := map[string]interface{}{"l": "warn", "key1": 1, "obj": testObject} 69 | l.Printf(toJSON(warn)) 70 | 71 | err := map[string]interface{}{"l": "error", "key1": 1, "obj": testObject} 72 | l.Printf(toJSON(err)) 73 | } 74 | b.StopTimer() 75 | } 76 | 77 | func BenchmarkLogxi(b *testing.B) { 78 | //fmt.Println("") 79 | stdout := log.NewConcurrentWriter(os.Stdout) 80 | l := log.NewLogger3(stdout, "bench", log.NewJSONFormatter("bench")) 81 | l.SetLevel(log.LevelDebug) 82 | 83 | b.ResetTimer() 84 | for i := 0; i < b.N; i++ { 85 | l.Debug("debug", "key", 1, "key2", "string", "key3", false) 86 | l.Info("info", "key", 1, "key2", "string", "key3", false) 87 | l.Warn("warn", "key", 1, "key2", "string", "key3", false) 88 | l.Error("error", "key", 1, "key2", "string", "key3", false) 89 | } 90 | b.StopTimer() 91 | } 92 | 93 | func BenchmarkLogxiComplex(b *testing.B) { 94 | //fmt.Println("") 95 | stdout := log.NewConcurrentWriter(os.Stdout) 96 | l := log.NewLogger3(stdout, "bench", log.NewJSONFormatter("bench")) 97 | l.SetLevel(log.LevelDebug) 98 | 99 | b.ResetTimer() 100 | for i := 0; i < b.N; i++ { 101 | l.Debug("debug", "key", 1, "obj", testObject) 102 | l.Info("info", "key", 1, "obj", testObject) 103 | l.Warn("warn", "key", 1, "obj", testObject) 104 | l.Error("error", "key", 1, "obj", testObject) 105 | } 106 | b.StopTimer() 107 | 108 | } 109 | 110 | func BenchmarkLogrus(b *testing.B) { 111 | //fmt.Println("") 112 | l := logrus.New() 113 | l.Formatter = &logrus.JSONFormatter{} 114 | 115 | b.ResetTimer() 116 | for i := 0; i < b.N; i++ { 117 | l.WithFields(logrus.Fields{"_n": "bench", "_p": pid, "key": 1, "key2": "string", "key3": false}).Debug("debug") 118 | l.WithFields(logrus.Fields{"_n": "bench", "_p": pid, "key": 1, "key2": "string", "key3": false}).Info("info") 119 | l.WithFields(logrus.Fields{"_n": "bench", "_p": pid, "key": 1, "key2": "string", "key3": false}).Warn("warn") 120 | l.WithFields(logrus.Fields{"_n": "bench", "_p": pid, "key": 1, "key2": "string", "key3": false}).Error("error") 121 | } 122 | b.StopTimer() 123 | } 124 | 125 | func BenchmarkLogrusComplex(b *testing.B) { 126 | //fmt.Println("") 127 | l := logrus.New() 128 | l.Formatter = &logrus.JSONFormatter{} 129 | 130 | b.ResetTimer() 131 | for i := 0; i < b.N; i++ { 132 | l.WithFields(logrus.Fields{"_n": "bench", "_p": pid, "key": 1, "obj": testObject}).Debug("debug") 133 | l.WithFields(logrus.Fields{"_n": "bench", "_p": pid, "key": 1, "obj": testObject}).Info("info") 134 | l.WithFields(logrus.Fields{"_n": "bench", "_p": pid, "key": 1, "obj": testObject}).Warn("warn") 135 | l.WithFields(logrus.Fields{"_n": "bench", "_p": pid, "key": 1, "obj": testObject}).Error("error") 136 | } 137 | b.StopTimer() 138 | } 139 | 140 | func BenchmarkLog15(b *testing.B) { 141 | //fmt.Println("") 142 | l := log15.New(log15.Ctx{"_n": "bench", "_p": pid}) 143 | l.SetHandler(log15.SyncHandler(log15.StreamHandler(os.Stdout, log15.JsonFormat()))) 144 | 145 | b.ResetTimer() 146 | for i := 0; i < b.N; i++ { 147 | l.Debug("debug", "key", 1, "key2", "string", "key3", false) 148 | l.Info("info", "key", 1, "key2", "string", "key3", false) 149 | l.Warn("warn", "key", 1, "key2", "string", "key3", false) 150 | l.Error("error", "key", 1, "key2", "string", "key3", false) 151 | } 152 | b.StopTimer() 153 | 154 | } 155 | 156 | func BenchmarkLog15Complex(b *testing.B) { 157 | //fmt.Println("") 158 | l := log15.New(log15.Ctx{"_n": "bench", "_p": pid}) 159 | l.SetHandler(log15.SyncHandler(log15.StreamHandler(os.Stdout, log15.JsonFormat()))) 160 | 161 | b.ResetTimer() 162 | for i := 0; i < b.N; i++ { 163 | l.Debug("debug", "key", 1, "obj", testObject) 164 | l.Info("info", "key", 1, "obj", testObject) 165 | l.Warn("warn", "key", 1, "obj", testObject) 166 | l.Error("error", "key", 1, "obj", testObject) 167 | } 168 | b.StopTimer() 169 | } 170 | -------------------------------------------------------------------------------- /v1/callstack.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/mgutz/ansi" 12 | ) 13 | 14 | type sourceLine struct { 15 | lineno int 16 | line string 17 | } 18 | 19 | type frameInfo struct { 20 | filename string 21 | lineno int 22 | method string 23 | context []*sourceLine 24 | contextLines int 25 | } 26 | 27 | func (ci *frameInfo) readSource(contextLines int) error { 28 | if ci.lineno == 0 || disableCallstack { 29 | return nil 30 | } 31 | start := maxInt(1, ci.lineno-contextLines) 32 | end := ci.lineno + contextLines 33 | 34 | f, err := os.Open(ci.filename) 35 | if err != nil { 36 | // if we can't read a file, it means user is running this in production 37 | disableCallstack = true 38 | return err 39 | } 40 | defer f.Close() 41 | 42 | lineno := 1 43 | scanner := bufio.NewScanner(f) 44 | for scanner.Scan() { 45 | if start <= lineno && lineno <= end { 46 | line := scanner.Text() 47 | line = expandTabs(line, 4) 48 | ci.context = append(ci.context, &sourceLine{lineno: lineno, line: line}) 49 | } 50 | lineno++ 51 | } 52 | 53 | if err := scanner.Err(); err != nil { 54 | InternalLog.Warn("scanner error", "file", ci.filename, "err", err) 55 | } 56 | return nil 57 | } 58 | 59 | func (ci *frameInfo) String(color string, sourceColor string) string { 60 | buf := pool.Get() 61 | defer pool.Put(buf) 62 | 63 | if disableCallstack { 64 | buf.WriteString(color) 65 | buf.WriteString(Separator) 66 | buf.WriteString(indent) 67 | buf.WriteString(ci.filename) 68 | buf.WriteRune(':') 69 | buf.WriteString(strconv.Itoa(ci.lineno)) 70 | return buf.String() 71 | } 72 | 73 | // skip anything in the logxi package 74 | if isLogxiCode(ci.filename) { 75 | return "" 76 | } 77 | 78 | // make path relative to current working directory or home 79 | tildeFilename, err := filepath.Rel(wd, ci.filename) 80 | if err != nil { 81 | InternalLog.Warn("Could not make path relative", "path", ci.filename) 82 | return "" 83 | } 84 | // ../../../ is too complex. Make path relative to home 85 | if strings.HasPrefix(tildeFilename, strings.Repeat(".."+string(os.PathSeparator), 3)) { 86 | tildeFilename = strings.Replace(tildeFilename, home, "~", 1) 87 | } 88 | 89 | buf.WriteString(color) 90 | buf.WriteString(Separator) 91 | buf.WriteString(indent) 92 | buf.WriteString("in ") 93 | buf.WriteString(ci.method) 94 | buf.WriteString("(") 95 | buf.WriteString(tildeFilename) 96 | buf.WriteRune(':') 97 | buf.WriteString(strconv.Itoa(ci.lineno)) 98 | buf.WriteString(")") 99 | 100 | if ci.contextLines == -1 { 101 | return buf.String() 102 | } 103 | buf.WriteString("\n") 104 | 105 | // the width of the printed line number 106 | var linenoWidth int 107 | // trim spaces at start of source code based on common spaces 108 | var skipSpaces = 1000 109 | 110 | // calculate width of lineno and number of leading spaces that can be 111 | // removed 112 | for _, li := range ci.context { 113 | linenoWidth = maxInt(linenoWidth, len(fmt.Sprintf("%d", li.lineno))) 114 | index := indexOfNonSpace(li.line) 115 | if index > -1 && index < skipSpaces { 116 | skipSpaces = index 117 | } 118 | } 119 | 120 | for _, li := range ci.context { 121 | var format string 122 | format = fmt.Sprintf("%%s%%%dd: %%s\n", linenoWidth) 123 | 124 | if li.lineno == ci.lineno { 125 | buf.WriteString(color) 126 | if ci.contextLines > 2 { 127 | format = fmt.Sprintf("%%s=> %%%dd: %%s\n", linenoWidth) 128 | } 129 | } else { 130 | buf.WriteString(sourceColor) 131 | if ci.contextLines > 2 { 132 | // account for "=> " 133 | format = fmt.Sprintf("%%s%%%dd: %%s\n", linenoWidth+3) 134 | } 135 | } 136 | // trim spaces at start 137 | idx := minInt(len(li.line), skipSpaces) 138 | buf.WriteString(fmt.Sprintf(format, Separator+indent+indent, li.lineno, li.line[idx:])) 139 | } 140 | // get rid of last \n 141 | buf.Truncate(buf.Len() - 1) 142 | if !disableColors { 143 | buf.WriteString(ansi.Reset) 144 | } 145 | return buf.String() 146 | } 147 | 148 | // parseDebugStack parases a stack created by debug.Stack() 149 | // 150 | // This is what the string looks like 151 | // /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:45 (0x5fa70) 152 | // (*JSONFormatter).writeError: jf.writeString(buf, err.Error()+"\n"+string(debug.Stack())) 153 | // /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:82 (0x5fdc3) 154 | // (*JSONFormatter).appendValue: jf.writeError(buf, err) 155 | // /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:109 (0x605ca) 156 | // (*JSONFormatter).set: jf.appendValue(buf, val) 157 | // ... 158 | // /Users/mgutz/goroot/src/runtime/asm_amd64.s:2232 (0x38bf1) 159 | // goexit: 160 | func parseDebugStack(stack string, skip int, ignoreRuntime bool) []*frameInfo { 161 | frames := []*frameInfo{} 162 | // BUG temporarily disable since there is a bug with embedded newlines 163 | if true { 164 | return frames 165 | } 166 | 167 | lines := strings.Split(stack, "\n") 168 | 169 | for i := skip * 2; i < len(lines); i += 2 { 170 | ci := &frameInfo{} 171 | sourceLine := lines[i] 172 | if sourceLine == "" { 173 | break 174 | } 175 | if ignoreRuntime && strings.Contains(sourceLine, filepath.Join("src", "runtime")) { 176 | break 177 | } 178 | 179 | colon := strings.Index(sourceLine, ":") 180 | slash := strings.Index(sourceLine, "/") 181 | if colon < slash { 182 | // must be on Windows where paths look like c:/foo/bar.go:lineno 183 | colon = strings.Index(sourceLine[slash:], ":") + slash 184 | } 185 | space := strings.Index(sourceLine, " ") 186 | ci.filename = sourceLine[0:colon] 187 | 188 | // BUG with callstack where the error message has embedded newlines 189 | // if colon > space { 190 | // fmt.Println("lines", lines) 191 | // } 192 | // fmt.Println("SOURCELINE", sourceLine, "len", len(sourceLine), "COLON", colon, "SPACE", space) 193 | numstr := sourceLine[colon+1 : space] 194 | lineno, err := strconv.Atoi(numstr) 195 | if err != nil { 196 | InternalLog.Warn("Could not parse line number", "sourceLine", sourceLine, "numstr", numstr) 197 | continue 198 | } 199 | ci.lineno = lineno 200 | 201 | methodLine := lines[i+1] 202 | colon = strings.Index(methodLine, ":") 203 | ci.method = strings.Trim(methodLine[0:colon], "\t ") 204 | frames = append(frames, ci) 205 | } 206 | return frames 207 | } 208 | 209 | // parseDebugStack parases a stack created by debug.Stack() 210 | // 211 | // This is what the string looks like 212 | // /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:45 (0x5fa70) 213 | // (*JSONFormatter).writeError: jf.writeString(buf, err.Error()+"\n"+string(debug.Stack())) 214 | // /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:82 (0x5fdc3) 215 | // (*JSONFormatter).appendValue: jf.writeError(buf, err) 216 | // /Users/mgutz/go/src/github.com/mgutz/logxi/v1/jsonFormatter.go:109 (0x605ca) 217 | // (*JSONFormatter).set: jf.appendValue(buf, val) 218 | // ... 219 | // /Users/mgutz/goroot/src/runtime/asm_amd64.s:2232 (0x38bf1) 220 | // goexit: 221 | func trimDebugStack(stack string) string { 222 | buf := pool.Get() 223 | defer pool.Put(buf) 224 | lines := strings.Split(stack, "\n") 225 | for i := 0; i < len(lines); i += 2 { 226 | sourceLine := lines[i] 227 | if sourceLine == "" { 228 | break 229 | } 230 | 231 | colon := strings.Index(sourceLine, ":") 232 | slash := strings.Index(sourceLine, "/") 233 | if colon < slash { 234 | // must be on Windows where paths look like c:/foo/bar.go:lineno 235 | colon = strings.Index(sourceLine[slash:], ":") + slash 236 | } 237 | filename := sourceLine[0:colon] 238 | // skip anything in the logxi package 239 | if isLogxiCode(filename) { 240 | continue 241 | } 242 | buf.WriteString(sourceLine) 243 | buf.WriteRune('\n') 244 | buf.WriteString(lines[i+1]) 245 | buf.WriteRune('\n') 246 | } 247 | return buf.String() 248 | } 249 | 250 | func parseLogxiStack(entry map[string]interface{}, skip int, ignoreRuntime bool) []*frameInfo { 251 | kv := entry[KeyMap.CallStack] 252 | if kv == nil { 253 | return nil 254 | } 255 | 256 | var frames []*frameInfo 257 | if stack, ok := kv.(string); ok { 258 | frames = parseDebugStack(stack, skip, ignoreRuntime) 259 | } 260 | return frames 261 | } 262 | -------------------------------------------------------------------------------- /v1/cmd/demo/main.ansi: -------------------------------------------------------------------------------- 1 | import  "github.com/mgutz/logxi/v1" 2 | 3 | func loadConfig() { 4 |  logger.Error("Could not read config file", "err", errConfig) 5 | } 6 | 7 | func main() { 8 |  // create loggers 9 |  log.Trace("creating loggers") 10 |  logger = log.New("server") 11 |  modelsLogger := log.New("models") 12 | 13 |  logger.Debug("Process", "hostname", hostname, "pid", os.Getpid()) 14 |  modelsLogger.Info("Connecting to database...") 15 |  modelsLogger.Warn("Could not connect, retrying ...", "dsn", dsn) 16 |  loadConfig() 17 | } 18 | -------------------------------------------------------------------------------- /v1/cmd/demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/mgutz/logxi/v1" 8 | ) 9 | 10 | var errConfig = fmt.Errorf("file not found") 11 | var dsn = "dbname=testdb" 12 | var logger log.Logger 13 | var hostname string 14 | var configFile = "config.json" 15 | 16 | func init() { 17 | hostname, _ = os.Hostname() 18 | } 19 | 20 | func loadConfig() { 21 | logger.Error("Could not read config file", "err", errConfig) 22 | } 23 | 24 | func main() { 25 | // create loggers 26 | log.Trace("creating loggers") 27 | logger = log.New("server") 28 | modelsLogger := log.New("models") 29 | 30 | logger.Debug("Process", "hostname", hostname, "pid", os.Getpid()) 31 | modelsLogger.Info("Connecting to database...") 32 | modelsLogger.Warn("Could not connect, retrying ...", "dsn", dsn) 33 | loadConfig() 34 | } 35 | -------------------------------------------------------------------------------- /v1/cmd/filter/README.md: -------------------------------------------------------------------------------- 1 | # filter 2 | 3 | Filter is an example of a how to process JSON log 4 | entries using pipes in your shell. 5 | 6 | ```sh 7 | yourapp | filter 8 | ``` 9 | 10 | You can try see it in action with `godo filter` 11 | -------------------------------------------------------------------------------- /v1/cmd/filter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/mgutz/logxi/v1" 11 | ) 12 | 13 | func sendExternal(obj map[string]interface{}) { 14 | // normally you would send this to an external service like InfluxDB 15 | // or some logging framework. Let's filter out some data. 16 | fmt.Printf("Time: %s Level: %s Message: %s\n", 17 | obj[log.KeyMap.Time], 18 | obj[log.KeyMap.Level], 19 | obj[log.KeyMap.Message], 20 | ) 21 | } 22 | 23 | func main() { 24 | r := bufio.NewReader(os.Stdin) 25 | dec := json.NewDecoder(r) 26 | for { 27 | var obj map[string]interface{} 28 | if err := dec.Decode(&obj); err == io.EOF { 29 | break 30 | } else if err != nil { 31 | log.InternalLog.Fatal("Could not decode", "err", err) 32 | } 33 | sendExternal(obj) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /v1/cmd/reldir/README.md: -------------------------------------------------------------------------------- 1 | # reldir 2 | 3 | Used to test relative paths when logging context. 4 | -------------------------------------------------------------------------------- /v1/cmd/reldir/foo.go: -------------------------------------------------------------------------------- 1 | package reldir 2 | 3 | import "github.com/mgutz/logxi/v1" 4 | 5 | // Foo returns error 6 | func Foo() { 7 | log.Error("Oh bar!") 8 | } 9 | -------------------------------------------------------------------------------- /v1/concurrentWriter.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | // ConcurrentWriter is a concurrent safe wrapper around io.Writer 9 | type ConcurrentWriter struct { 10 | writer io.Writer 11 | sync.Mutex 12 | } 13 | 14 | // NewConcurrentWriter crates a new concurrent writer wrapper around existing writer. 15 | func NewConcurrentWriter(writer io.Writer) io.Writer { 16 | return &ConcurrentWriter{writer: writer} 17 | } 18 | 19 | func (cw *ConcurrentWriter) Write(p []byte) (n int, err error) { 20 | cw.Lock() 21 | defer cw.Unlock() 22 | // This is basically the same logic as in go's log.Output() which 23 | // doesn't look at the returned number of bytes returned 24 | return cw.writer.Write(p) 25 | } 26 | -------------------------------------------------------------------------------- /v1/defaultLogger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // DefaultLogger is the default logger for this package. 9 | type DefaultLogger struct { 10 | writer io.Writer 11 | name string 12 | level int 13 | formatter Formatter 14 | } 15 | 16 | // NewLogger creates a new default logger. If writer is not concurrent 17 | // safe, wrap it with NewConcurrentWriter. 18 | func NewLogger(writer io.Writer, name string) Logger { 19 | formatter, err := createFormatter(name, logxiFormat) 20 | if err != nil { 21 | panic("Could not create formatter") 22 | } 23 | return NewLogger3(writer, name, formatter) 24 | } 25 | 26 | // NewLogger3 creates a new logger with a writer, name and formatter. If writer is not concurrent 27 | // safe, wrap it with NewConcurrentWriter. 28 | func NewLogger3(writer io.Writer, name string, formatter Formatter) Logger { 29 | var level int 30 | if name != "__logxi" { 31 | // if err is returned, then it means the log is disabled 32 | level = getLogLevel(name) 33 | if level == LevelOff { 34 | return NullLog 35 | } 36 | } 37 | 38 | log := &DefaultLogger{ 39 | formatter: formatter, 40 | writer: writer, 41 | name: name, 42 | level: level, 43 | } 44 | 45 | // TODO loggers will be used when watching changes to configuration such 46 | // as in consul, etcd 47 | loggers.Lock() 48 | loggers.loggers[name] = log 49 | loggers.Unlock() 50 | return log 51 | } 52 | 53 | // New creates a colorable default logger. 54 | func New(name string) Logger { 55 | return NewLogger(colorableStdout, name) 56 | } 57 | 58 | // Trace logs a debug entry. 59 | func (l *DefaultLogger) Trace(msg string, args ...interface{}) { 60 | l.Log(LevelTrace, msg, args) 61 | } 62 | 63 | // Debug logs a debug entry. 64 | func (l *DefaultLogger) Debug(msg string, args ...interface{}) { 65 | l.Log(LevelDebug, msg, args) 66 | } 67 | 68 | // Info logs an info entry. 69 | func (l *DefaultLogger) Info(msg string, args ...interface{}) { 70 | l.Log(LevelInfo, msg, args) 71 | } 72 | 73 | // Warn logs a warn entry. 74 | func (l *DefaultLogger) Warn(msg string, args ...interface{}) error { 75 | if l.IsWarn() { 76 | defer l.Log(LevelWarn, msg, args) 77 | 78 | for _, arg := range args { 79 | if err, ok := arg.(error); ok { 80 | return err 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | return nil 87 | } 88 | 89 | func (l *DefaultLogger) extractLogError(level int, msg string, args []interface{}) error { 90 | defer l.Log(level, msg, args) 91 | 92 | for _, arg := range args { 93 | if err, ok := arg.(error); ok { 94 | return err 95 | } 96 | } 97 | return fmt.Errorf(msg) 98 | } 99 | 100 | // Error logs an error entry. 101 | func (l *DefaultLogger) Error(msg string, args ...interface{}) error { 102 | return l.extractLogError(LevelError, msg, args) 103 | } 104 | 105 | // Fatal logs a fatal entry then panics. 106 | func (l *DefaultLogger) Fatal(msg string, args ...interface{}) { 107 | l.extractLogError(LevelFatal, msg, args) 108 | defer panic("Exit due to fatal error: ") 109 | } 110 | 111 | // Log logs a leveled entry. 112 | func (l *DefaultLogger) Log(level int, msg string, args []interface{}) { 113 | // log if the log level (warn=4) >= level of message (err=3) 114 | if l.level < level || silent { 115 | return 116 | } 117 | l.formatter.Format(l.writer, level, msg, args) 118 | } 119 | 120 | // IsTrace determines if this logger logs a debug statement. 121 | func (l *DefaultLogger) IsTrace() bool { 122 | // DEBUG(7) >= TRACE(10) 123 | return l.level >= LevelTrace 124 | } 125 | 126 | // IsDebug determines if this logger logs a debug statement. 127 | func (l *DefaultLogger) IsDebug() bool { 128 | return l.level >= LevelDebug 129 | } 130 | 131 | // IsInfo determines if this logger logs an info statement. 132 | func (l *DefaultLogger) IsInfo() bool { 133 | return l.level >= LevelInfo 134 | } 135 | 136 | // IsWarn determines if this logger logs a warning statement. 137 | func (l *DefaultLogger) IsWarn() bool { 138 | return l.level >= LevelWarn 139 | } 140 | 141 | // SetLevel sets the level of this logger. 142 | func (l *DefaultLogger) SetLevel(level int) { 143 | l.level = level 144 | } 145 | 146 | // SetFormatter set the formatter for this logger. 147 | func (l *DefaultLogger) SetFormatter(formatter Formatter) { 148 | l.formatter = formatter 149 | } 150 | -------------------------------------------------------------------------------- /v1/env.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | var contextLines int 10 | 11 | // Configuration comes from environment or external services like 12 | // consul, etcd. 13 | type Configuration struct { 14 | Format string `json:"format"` 15 | Colors string `json:"colors"` 16 | Levels string `json:"levels"` 17 | } 18 | 19 | func readFromEnviron() *Configuration { 20 | conf := &Configuration{} 21 | 22 | var envOrDefault = func(name, val string) string { 23 | result := os.Getenv(name) 24 | if result == "" { 25 | result = val 26 | } 27 | return result 28 | } 29 | 30 | conf.Levels = envOrDefault("LOGXI", defaultLogxiEnv) 31 | conf.Format = envOrDefault("LOGXI_FORMAT", defaultLogxiFormatEnv) 32 | conf.Colors = envOrDefault("LOGXI_COLORS", defaultLogxiColorsEnv) 33 | return conf 34 | } 35 | 36 | // ProcessEnv (re)processes environment. 37 | func ProcessEnv(env *Configuration) { 38 | // TODO: allow reading from etcd 39 | 40 | ProcessLogxiEnv(env.Levels) 41 | ProcessLogxiColorsEnv(env.Colors) 42 | ProcessLogxiFormatEnv(env.Format) 43 | } 44 | 45 | // ProcessLogxiFormatEnv parses LOGXI_FORMAT 46 | func ProcessLogxiFormatEnv(env string) { 47 | logxiFormat = env 48 | m := parseKVList(logxiFormat, ",") 49 | formatterFormat := "" 50 | tFormat := "" 51 | for key, value := range m { 52 | switch key { 53 | default: 54 | formatterFormat = key 55 | case "t": 56 | tFormat = value 57 | case "pretty": 58 | isPretty = value != "false" && value != "0" 59 | case "maxcol": 60 | col, err := strconv.Atoi(value) 61 | if err == nil { 62 | maxCol = col 63 | } else { 64 | maxCol = defaultMaxCol 65 | } 66 | case "context": 67 | lines, err := strconv.Atoi(value) 68 | if err == nil { 69 | contextLines = lines 70 | } else { 71 | contextLines = defaultContextLines 72 | } 73 | case "LTSV": 74 | formatterFormat = "text" 75 | AssignmentChar = ltsvAssignmentChar 76 | Separator = ltsvSeparator 77 | } 78 | } 79 | if formatterFormat == "" || formatterCreators[formatterFormat] == nil { 80 | formatterFormat = defaultFormat 81 | } 82 | logxiFormat = formatterFormat 83 | if tFormat == "" { 84 | tFormat = defaultTimeFormat 85 | } 86 | timeFormat = tFormat 87 | } 88 | 89 | // ProcessLogxiEnv parses LOGXI variable 90 | func ProcessLogxiEnv(env string) { 91 | logxiEnable := env 92 | if logxiEnable == "" { 93 | logxiEnable = defaultLogxiEnv 94 | } 95 | 96 | logxiNameLevelMap = map[string]int{} 97 | m := parseKVList(logxiEnable, ",") 98 | if m == nil { 99 | logxiNameLevelMap["*"] = defaultLevel 100 | } 101 | for key, value := range m { 102 | if strings.HasPrefix(key, "-") { 103 | // LOGXI=*,-foo => disable foo 104 | logxiNameLevelMap[key[1:]] = LevelOff 105 | } else if value == "" { 106 | // LOGXI=* => default to all 107 | logxiNameLevelMap[key] = LevelAll 108 | } else { 109 | // LOGXI=*=ERR => use user-specified level 110 | level := LevelAtoi[value] 111 | if level == 0 { 112 | InternalLog.Error("Unknown level in LOGXI environment variable", "key", key, "value", value, "LOGXI", env) 113 | level = defaultLevel 114 | } 115 | logxiNameLevelMap[key] = level 116 | } 117 | } 118 | 119 | // must always have global default, otherwise errs may get eaten up 120 | if _, ok := logxiNameLevelMap["*"]; !ok { 121 | logxiNameLevelMap["*"] = LevelError 122 | } 123 | } 124 | 125 | func getLogLevel(name string) int { 126 | var wildcardLevel int 127 | var result int 128 | 129 | for k, v := range logxiNameLevelMap { 130 | if k == name { 131 | result = v 132 | } else if k == "*" { 133 | wildcardLevel = v 134 | } else if strings.HasPrefix(k, "*") && strings.HasSuffix(name, k[1:]) { 135 | result = v 136 | } else if strings.HasSuffix(k, "*") && strings.HasPrefix(name, k[:len(k)-1]) { 137 | result = v 138 | } 139 | } 140 | 141 | if result == LevelOff { 142 | return LevelOff 143 | } 144 | 145 | if result > 0 { 146 | return result 147 | } 148 | 149 | if wildcardLevel > 0 { 150 | return wildcardLevel 151 | } 152 | 153 | return LevelOff 154 | } 155 | 156 | // ProcessLogxiColorsEnv parases LOGXI_COLORS 157 | func ProcessLogxiColorsEnv(env string) { 158 | colors := env 159 | if colors == "" { 160 | colors = defaultLogxiColorsEnv 161 | } else if colors == "*=off" { 162 | // disable all colors 163 | disableColors = true 164 | } 165 | theme = parseTheme(colors) 166 | } 167 | -------------------------------------------------------------------------------- /v1/formatter.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | var formatterCreators = map[string]CreateFormatterFunc{} 4 | 5 | // CreateFormatterFunc is a function which creates a new instance 6 | // of a Formatter. 7 | type CreateFormatterFunc func(name, kind string) (Formatter, error) 8 | 9 | // createFormatter creates formatters. It accepts a kind in {"text", "JSON"} 10 | // which correspond to TextFormatter and JSONFormatter, and the name of the 11 | // logger. 12 | func createFormatter(name string, kind string) (Formatter, error) { 13 | if kind == FormatEnv { 14 | kind = logxiFormat 15 | } 16 | if kind == "" { 17 | kind = FormatText 18 | } 19 | 20 | fn := formatterCreators[kind] 21 | if fn == nil { 22 | fn = formatterCreators[FormatText] 23 | } 24 | 25 | formatter, err := fn(name, kind) 26 | if err != nil { 27 | return nil, err 28 | } 29 | // custom formatter may have not returned a formatter 30 | if formatter == nil { 31 | formatter, err = formatFactory(name, FormatText) 32 | } 33 | return formatter, err 34 | } 35 | 36 | func formatFactory(name string, kind string) (Formatter, error) { 37 | var formatter Formatter 38 | var err error 39 | switch kind { 40 | default: 41 | formatter = NewTextFormatter(name) 42 | case FormatHappy: 43 | formatter = NewHappyDevFormatter(name) 44 | case FormatText: 45 | formatter = NewTextFormatter(name) 46 | case FormatJSON: 47 | formatter = NewJSONFormatter(name) 48 | } 49 | return formatter, err 50 | } 51 | 52 | // RegisterFormatFactory registers a format factory function. 53 | func RegisterFormatFactory(kind string, fn CreateFormatterFunc) { 54 | if kind == "" { 55 | panic("kind is empty string") 56 | } 57 | if fn == nil { 58 | panic("creator is nil") 59 | } 60 | formatterCreators[kind] = fn 61 | } 62 | -------------------------------------------------------------------------------- /v1/happyDevFormatter.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "runtime/debug" 8 | "strings" 9 | 10 | "github.com/mgutz/ansi" 11 | ) 12 | 13 | // colorScheme defines a color theme for HappyDevFormatter 14 | type colorScheme struct { 15 | Key string 16 | Message string 17 | Value string 18 | Misc string 19 | Source string 20 | 21 | Trace string 22 | Debug string 23 | Info string 24 | Warn string 25 | Error string 26 | } 27 | 28 | var indent = " " 29 | var maxCol = defaultMaxCol 30 | var theme *colorScheme 31 | 32 | func parseKVList(s, separator string) map[string]string { 33 | pairs := strings.Split(s, separator) 34 | if len(pairs) == 0 { 35 | return nil 36 | } 37 | m := map[string]string{} 38 | for _, pair := range pairs { 39 | if pair == "" { 40 | continue 41 | } 42 | parts := strings.Split(pair, "=") 43 | switch len(parts) { 44 | case 1: 45 | m[parts[0]] = "" 46 | case 2: 47 | m[parts[0]] = parts[1] 48 | } 49 | } 50 | return m 51 | } 52 | 53 | func parseTheme(theme string) *colorScheme { 54 | m := parseKVList(theme, ",") 55 | cs := &colorScheme{} 56 | var wildcard string 57 | 58 | var color = func(key string) string { 59 | if disableColors { 60 | return "" 61 | } 62 | style := m[key] 63 | c := ansi.ColorCode(style) 64 | if c == "" { 65 | c = wildcard 66 | } 67 | //fmt.Printf("plain=%b [%s] %s=%q\n", ansi.DefaultFG, key, style, c) 68 | 69 | return c 70 | } 71 | wildcard = color("*") 72 | 73 | if wildcard != ansi.Reset { 74 | cs.Key = wildcard 75 | cs.Value = wildcard 76 | cs.Misc = wildcard 77 | cs.Source = wildcard 78 | cs.Message = wildcard 79 | 80 | cs.Trace = wildcard 81 | cs.Debug = wildcard 82 | cs.Warn = wildcard 83 | cs.Info = wildcard 84 | cs.Error = wildcard 85 | } 86 | 87 | cs.Key = color("key") 88 | cs.Value = color("value") 89 | cs.Misc = color("misc") 90 | cs.Source = color("source") 91 | cs.Message = color("message") 92 | 93 | cs.Trace = color("TRC") 94 | cs.Debug = color("DBG") 95 | cs.Warn = color("WRN") 96 | cs.Info = color("INF") 97 | cs.Error = color("ERR") 98 | return cs 99 | } 100 | 101 | // HappyDevFormatter is the formatter used for terminals. It is 102 | // colorful, dev friendly and provides meaningful logs when 103 | // warnings and errors occur. 104 | // 105 | // HappyDevFormatter does not worry about performance. It's at least 3-4X 106 | // slower than JSONFormatter since it delegates to JSONFormatter to marshal 107 | // then unmarshal JSON. Then it does other stuff like read source files, sort 108 | // keys all to give a developer more information. 109 | // 110 | // SHOULD NOT be used in production for extended period of time. However, it 111 | // works fine in SSH terminals and binary deployments. 112 | type HappyDevFormatter struct { 113 | name string 114 | col int 115 | // always use the production formatter 116 | jsonFormatter *JSONFormatter 117 | } 118 | 119 | // NewHappyDevFormatter returns a new instance of HappyDevFormatter. 120 | func NewHappyDevFormatter(name string) *HappyDevFormatter { 121 | jf := NewJSONFormatter(name) 122 | return &HappyDevFormatter{ 123 | name: name, 124 | jsonFormatter: jf, 125 | } 126 | } 127 | 128 | func (hd *HappyDevFormatter) writeKey(buf bufferWriter, key string) { 129 | // assumes this is not the first key 130 | hd.writeString(buf, Separator) 131 | if key == "" { 132 | return 133 | } 134 | buf.WriteString(theme.Key) 135 | hd.writeString(buf, key) 136 | hd.writeString(buf, AssignmentChar) 137 | if !disableColors { 138 | buf.WriteString(ansi.Reset) 139 | } 140 | } 141 | 142 | func (hd *HappyDevFormatter) set(buf bufferWriter, key string, value interface{}, color string) { 143 | var str string 144 | if s, ok := value.(string); ok { 145 | str = s 146 | } else if s, ok := value.(fmt.Stringer); ok { 147 | str = s.String() 148 | } else { 149 | str = fmt.Sprintf("%v", value) 150 | } 151 | val := strings.Trim(str, "\n ") 152 | if (isPretty && key != "") || hd.col+len(key)+2+len(val) >= maxCol { 153 | buf.WriteString("\n") 154 | hd.col = 0 155 | hd.writeString(buf, indent) 156 | } 157 | hd.writeKey(buf, key) 158 | if color != "" { 159 | buf.WriteString(color) 160 | } 161 | hd.writeString(buf, val) 162 | if color != "" && !disableColors { 163 | buf.WriteString(ansi.Reset) 164 | } 165 | } 166 | 167 | // Write a string and tracks the position of the string so we can break lines 168 | // cleanly. Do not send ANSI escape sequences, just raw strings 169 | func (hd *HappyDevFormatter) writeString(buf bufferWriter, s string) { 170 | buf.WriteString(s) 171 | hd.col += len(s) 172 | } 173 | 174 | func (hd *HappyDevFormatter) getContext(color string) string { 175 | if disableCallstack { 176 | return "" 177 | } 178 | frames := parseDebugStack(string(debug.Stack()), 5, true) 179 | if len(frames) == 0 { 180 | return "" 181 | } 182 | for _, frame := range frames { 183 | context := frame.String(color, theme.Source) 184 | if context != "" { 185 | return context 186 | } 187 | } 188 | return "" 189 | } 190 | 191 | func (hd *HappyDevFormatter) getLevelContext(level int, entry map[string]interface{}) (message string, context string, color string) { 192 | 193 | switch level { 194 | case LevelTrace: 195 | color = theme.Trace 196 | context = hd.getContext(color) 197 | context += "\n" 198 | case LevelDebug: 199 | color = theme.Debug 200 | case LevelInfo: 201 | color = theme.Info 202 | // case LevelWarn: 203 | // color = theme.Warn 204 | // context = hd.getContext(color) 205 | // context += "\n" 206 | case LevelWarn, LevelError, LevelFatal: 207 | 208 | // warnings return an error but if it does not have an error 209 | // then print line info only 210 | if level == LevelWarn { 211 | color = theme.Warn 212 | kv := entry[KeyMap.CallStack] 213 | if kv == nil { 214 | context = hd.getContext(color) 215 | context += "\n" 216 | break 217 | } 218 | } else { 219 | color = theme.Error 220 | } 221 | 222 | if disableCallstack || contextLines == -1 { 223 | context = trimDebugStack(string(debug.Stack())) 224 | break 225 | } 226 | frames := parseLogxiStack(entry, 4, true) 227 | if frames == nil { 228 | frames = parseDebugStack(string(debug.Stack()), 4, true) 229 | } 230 | 231 | if len(frames) == 0 { 232 | break 233 | } 234 | errbuf := pool.Get() 235 | defer pool.Put(errbuf) 236 | lines := 0 237 | for _, frame := range frames { 238 | err := frame.readSource(contextLines) 239 | if err != nil { 240 | // by setting to empty, the original stack is used 241 | errbuf.Reset() 242 | break 243 | } 244 | ctx := frame.String(color, theme.Source) 245 | if ctx == "" { 246 | continue 247 | } 248 | errbuf.WriteString(ctx) 249 | errbuf.WriteRune('\n') 250 | lines++ 251 | } 252 | context = errbuf.String() 253 | default: 254 | panic("should never get here") 255 | } 256 | return message, context, color 257 | } 258 | 259 | // Format a log entry. 260 | func (hd *HappyDevFormatter) Format(writer io.Writer, level int, msg string, args []interface{}) { 261 | buf := pool.Get() 262 | defer pool.Put(buf) 263 | 264 | if len(args) == 1 { 265 | args = append(args, 0) 266 | copy(args[1:], args[0:]) 267 | args[0] = singleArgKey 268 | } 269 | 270 | // warn about reserved, bad and complex keys 271 | for i := 0; i < len(args); i += 2 { 272 | isReserved, err := isReservedKey(args[i]) 273 | if err != nil { 274 | InternalLog.Error("Key is not a string.", "err", fmt.Errorf("args[%d]=%v", i, args[i])) 275 | } else if isReserved { 276 | InternalLog.Fatal("Key conflicts with reserved key. Avoiding using single rune keys.", "key", args[i].(string)) 277 | } else { 278 | // Ensure keys are simple strings. The JSONFormatter doesn't escape 279 | // keys as a performance tradeoff. This panics if the JSON key 280 | // value has a different value than a simple quoted string. 281 | key := args[i].(string) 282 | b, err := json.Marshal(key) 283 | if err != nil { 284 | panic("Key is invalid. " + err.Error()) 285 | } 286 | if string(b) != `"`+key+`"` { 287 | panic("Key is complex. Use simpler key for: " + fmt.Sprintf("%q", key)) 288 | } 289 | } 290 | } 291 | 292 | // use the production JSON formatter to format the log first. This 293 | // ensures JSON will marshal/unmarshal correctly in production. 294 | entry := hd.jsonFormatter.LogEntry(level, msg, args) 295 | 296 | // reset the column tracker used for fancy formatting 297 | hd.col = 0 298 | 299 | // timestamp 300 | buf.WriteString(theme.Misc) 301 | hd.writeString(buf, entry[KeyMap.Time].(string)) 302 | if !disableColors { 303 | buf.WriteString(ansi.Reset) 304 | } 305 | 306 | // emphasize warnings and errors 307 | message, context, color := hd.getLevelContext(level, entry) 308 | if message == "" { 309 | message = entry[KeyMap.Message].(string) 310 | } 311 | 312 | // DBG, INF ... 313 | hd.set(buf, "", entry[KeyMap.Level].(string), color) 314 | // logger name 315 | hd.set(buf, "", entry[KeyMap.Name], theme.Misc) 316 | // message from user 317 | hd.set(buf, "", message, theme.Message) 318 | 319 | // Preserve key order in the sequencethey were added by developer.This 320 | // makes it easier for developers to follow the log. 321 | order := []string{} 322 | lenArgs := len(args) 323 | for i := 0; i < len(args); i += 2 { 324 | if i+1 >= lenArgs { 325 | continue 326 | } 327 | if key, ok := args[i].(string); ok { 328 | order = append(order, key) 329 | } else { 330 | order = append(order, badKeyAtIndex(i)) 331 | } 332 | } 333 | 334 | for _, key := range order { 335 | // skip reserved keys which were already added to buffer above 336 | isReserved, err := isReservedKey(key) 337 | if err != nil { 338 | panic("key is invalid. Should never get here. " + err.Error()) 339 | } else if isReserved { 340 | continue 341 | } 342 | hd.set(buf, key, entry[key], theme.Value) 343 | } 344 | 345 | addLF := true 346 | hasCallStack := entry[KeyMap.CallStack] != nil 347 | // WRN,ERR file, line number context 348 | 349 | if context != "" { 350 | // warnings and traces are single line, space can be optimized 351 | if level == LevelTrace || (level == LevelWarn && !hasCallStack) { 352 | // gets rid of "in " 353 | idx := strings.IndexRune(context, 'n') 354 | hd.set(buf, "in", context[idx+2:], color) 355 | } else { 356 | buf.WriteRune('\n') 357 | if !disableColors { 358 | buf.WriteString(color) 359 | } 360 | addLF = context[len(context)-1:len(context)] != "\n" 361 | buf.WriteString(context) 362 | if !disableColors { 363 | buf.WriteString(ansi.Reset) 364 | } 365 | } 366 | } else if hasCallStack { 367 | hd.set(buf, "", entry[KeyMap.CallStack], color) 368 | } 369 | if addLF { 370 | buf.WriteRune('\n') 371 | } 372 | buf.WriteTo(writer) 373 | } 374 | -------------------------------------------------------------------------------- /v1/init.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "runtime" 8 | "strconv" 9 | "sync" 10 | 11 | "github.com/mattn/go-colorable" 12 | "github.com/mattn/go-isatty" 13 | ) 14 | 15 | // scream so user fixes it 16 | const warnImbalancedKey = "FIX_IMBALANCED_PAIRS" 17 | const warnImbalancedPairs = warnImbalancedKey + " => " 18 | const singleArgKey = "_" 19 | 20 | func badKeyAtIndex(i int) string { 21 | return "BAD_KEY_AT_INDEX_" + strconv.Itoa(i) 22 | } 23 | 24 | // DefaultLogLog is the default log for this package. 25 | var DefaultLog Logger 26 | 27 | // Suppress supresses logging and is useful to supress output in 28 | // in unit tests. 29 | // 30 | // Example 31 | // log.Suppress(true) 32 | // defer log.suppress(false) 33 | func Suppress(quiet bool) { 34 | silent = quiet 35 | } 36 | 37 | var silent bool 38 | 39 | // internalLog is the logger used by logxi itself 40 | var InternalLog Logger 41 | 42 | type loggerMap struct { 43 | sync.Mutex 44 | loggers map[string]Logger 45 | } 46 | 47 | var loggers = &loggerMap{ 48 | loggers: map[string]Logger{}, 49 | } 50 | 51 | func (lm *loggerMap) set(name string, logger Logger) { 52 | lm.loggers[name] = logger 53 | } 54 | 55 | // The assignment character between key-value pairs 56 | var AssignmentChar = ": " 57 | 58 | // Separator is the separator to use between key value pairs 59 | //var Separator = "{~}" 60 | var Separator = " " 61 | 62 | const ltsvAssignmentChar = ":" 63 | const ltsvSeparator = "\t" 64 | 65 | // logxiEnabledMap maps log name patterns to levels 66 | var logxiNameLevelMap map[string]int 67 | 68 | // logxiFormat is the formatter kind to create 69 | var logxiFormat string 70 | 71 | var colorableStdout io.Writer 72 | var defaultContextLines = 2 73 | var defaultFormat string 74 | var defaultLevel int 75 | var defaultLogxiEnv string 76 | var defaultLogxiFormatEnv string 77 | var defaultMaxCol = 80 78 | var defaultPretty = false 79 | var defaultLogxiColorsEnv string 80 | var defaultTimeFormat string 81 | var disableCallstack bool 82 | var disableCheckKeys bool 83 | var disableColors bool 84 | var home string 85 | var isPretty bool 86 | var isTerminal bool 87 | var isWindows = runtime.GOOS == "windows" 88 | var pkgMutex sync.Mutex 89 | var pool = NewBufferPool() 90 | var timeFormat string 91 | var wd string 92 | var pid = os.Getpid() 93 | var pidStr = strconv.Itoa(os.Getpid()) 94 | 95 | // KeyMapping is the key map used to print built-in log entry fields. 96 | type KeyMapping struct { 97 | Level string 98 | Message string 99 | Name string 100 | PID string 101 | Time string 102 | CallStack string 103 | } 104 | 105 | // KeyMap is the key map to use when printing log statements. 106 | var KeyMap = &KeyMapping{ 107 | Level: "_l", 108 | Message: "_m", 109 | Name: "_n", 110 | PID: "_p", 111 | Time: "_t", 112 | CallStack: "_c", 113 | } 114 | 115 | var logxiKeys []string 116 | 117 | func setDefaults(isTerminal bool) { 118 | var err error 119 | contextLines = defaultContextLines 120 | wd, err = os.Getwd() 121 | if err != nil { 122 | InternalLog.Error("Could not get working directory") 123 | } 124 | 125 | logxiKeys = []string{KeyMap.Level, KeyMap.Message, KeyMap.Name, KeyMap.Time, KeyMap.CallStack, KeyMap.PID} 126 | 127 | if isTerminal { 128 | defaultLogxiEnv = "*=WRN" 129 | defaultLogxiFormatEnv = "happy,fit,maxcol=80,t=15:04:05.000000,context=-1" 130 | defaultFormat = FormatHappy 131 | defaultLevel = LevelWarn 132 | defaultTimeFormat = "15:04:05.000000" 133 | } else { 134 | defaultLogxiEnv = "*=ERR" 135 | defaultLogxiFormatEnv = "JSON,t=2006-01-02T15:04:05-0700" 136 | defaultFormat = FormatJSON 137 | defaultLevel = LevelError 138 | defaultTimeFormat = "2006-01-02T15:04:05-0700" 139 | disableColors = true 140 | } 141 | 142 | if isWindows { 143 | home = os.Getenv("HOMEPATH") 144 | if os.Getenv("ConEmuANSI") == "ON" { 145 | defaultLogxiColorsEnv = "key=cyan+h,value,misc=blue+h,source=yellow,TRC,DBG,WRN=yellow+h,INF=green+h,ERR=red+h" 146 | } else { 147 | colorableStdout = NewConcurrentWriter(colorable.NewColorableStdout()) 148 | defaultLogxiColorsEnv = "ERR=red,misc=cyan,key=cyan" 149 | } 150 | // DefaultScheme is a color scheme optimized for dark background 151 | // but works well with light backgrounds 152 | } else { 153 | home = os.Getenv("HOME") 154 | term := os.Getenv("TERM") 155 | if term == "xterm-256color" { 156 | defaultLogxiColorsEnv = "key=cyan+h,value,misc=blue,source=88,TRC,DBG,WRN=yellow,INF=green+h,ERR=red+h,message=magenta+h" 157 | } else { 158 | defaultLogxiColorsEnv = "key=cyan+h,value,misc=blue,source=magenta,TRC,DBG,WRN=yellow,INF=green,ERR=red+h" 159 | } 160 | } 161 | } 162 | 163 | func isReservedKey(k interface{}) (bool, error) { 164 | key, ok := k.(string) 165 | if !ok { 166 | return false, fmt.Errorf("Key is not a string") 167 | } 168 | 169 | // check if reserved 170 | for _, key2 := range logxiKeys { 171 | if key == key2 { 172 | return true, nil 173 | } 174 | } 175 | return false, nil 176 | } 177 | 178 | func init() { 179 | colorableStdout = NewConcurrentWriter(os.Stdout) 180 | 181 | isTerminal = isatty.IsTerminal(os.Stdout.Fd()) 182 | 183 | // the internal logger to report errors 184 | if isTerminal { 185 | InternalLog = NewLogger3(NewConcurrentWriter(os.Stdout), "__logxi", NewTextFormatter("__logxi")) 186 | } else { 187 | InternalLog = NewLogger3(NewConcurrentWriter(os.Stdout), "__logxi", NewJSONFormatter("__logxi")) 188 | } 189 | InternalLog.SetLevel(LevelError) 190 | 191 | setDefaults(isTerminal) 192 | 193 | RegisterFormatFactory(FormatHappy, formatFactory) 194 | RegisterFormatFactory(FormatText, formatFactory) 195 | RegisterFormatFactory(FormatJSON, formatFactory) 196 | ProcessEnv(readFromEnviron()) 197 | 198 | // package logger for users 199 | DefaultLog = New("~") 200 | } 201 | -------------------------------------------------------------------------------- /v1/init_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var testBuf bytes.Buffer 12 | 13 | var testInternalLog Logger 14 | 15 | func init() { 16 | testInternalLog = NewLogger3(&testBuf, "__logxi", NewTextFormatter("__logxi")) 17 | testInternalLog.SetLevel(LevelError) 18 | } 19 | 20 | func TestUnknownLevel(t *testing.T) { 21 | testResetEnv() 22 | os.Setenv("LOGXI", "*=oy") 23 | processEnv() 24 | buffer := testBuf.String() 25 | assert.Contains(t, buffer, "Unknown level", "should error on unknown level") 26 | } 27 | -------------------------------------------------------------------------------- /v1/jsonFormatter.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "runtime/debug" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | type bufferWriter interface { 14 | Write(p []byte) (nn int, err error) 15 | WriteRune(r rune) (n int, err error) 16 | WriteString(s string) (n int, err error) 17 | } 18 | 19 | // JSONFormatter is a fast, efficient JSON formatter optimized for logging. 20 | // 21 | // * log entry keys are not escaped 22 | // Who uses complex keys when coding? Checked by HappyDevFormatter in case user does. 23 | // Nested object keys are escaped by json.Marshal(). 24 | // * Primitive types uses strconv 25 | // * Logger reserved key values (time, log name, level) require no conversion 26 | // * sync.Pool buffer for bytes.Buffer 27 | type JSONFormatter struct { 28 | name string 29 | } 30 | 31 | // NewJSONFormatter creates a new instance of JSONFormatter. 32 | func NewJSONFormatter(name string) *JSONFormatter { 33 | return &JSONFormatter{name: name} 34 | } 35 | 36 | func (jf *JSONFormatter) writeString(buf bufferWriter, s string) { 37 | b, err := json.Marshal(s) 38 | if err != nil { 39 | InternalLog.Error("Could not json.Marshal string.", "str", s) 40 | buf.WriteString(`"Could not marshal this key's string"`) 41 | return 42 | } 43 | buf.Write(b) 44 | } 45 | 46 | func (jf *JSONFormatter) writeError(buf bufferWriter, err error) { 47 | jf.writeString(buf, err.Error()) 48 | jf.set(buf, KeyMap.CallStack, string(debug.Stack())) 49 | return 50 | } 51 | 52 | func (jf *JSONFormatter) appendValue(buf bufferWriter, val interface{}) { 53 | if val == nil { 54 | buf.WriteString("null") 55 | return 56 | } 57 | 58 | // always show error stack even at cost of some performance. there's 59 | // nothing worse than looking at production logs without a clue 60 | if err, ok := val.(error); ok { 61 | jf.writeError(buf, err) 62 | return 63 | } 64 | 65 | value := reflect.ValueOf(val) 66 | kind := value.Kind() 67 | if kind == reflect.Ptr { 68 | if value.IsNil() { 69 | buf.WriteString("null") 70 | return 71 | } 72 | value = value.Elem() 73 | kind = value.Kind() 74 | } 75 | switch kind { 76 | case reflect.Bool: 77 | if value.Bool() { 78 | buf.WriteString("true") 79 | } else { 80 | buf.WriteString("false") 81 | } 82 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 83 | buf.WriteString(strconv.FormatInt(value.Int(), 10)) 84 | 85 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 86 | buf.WriteString(strconv.FormatUint(value.Uint(), 10)) 87 | 88 | case reflect.Float32: 89 | buf.WriteString(strconv.FormatFloat(value.Float(), 'g', -1, 32)) 90 | 91 | case reflect.Float64: 92 | buf.WriteString(strconv.FormatFloat(value.Float(), 'g', -1, 64)) 93 | 94 | default: 95 | var err error 96 | var b []byte 97 | if stringer, ok := val.(fmt.Stringer); ok { 98 | b, err = json.Marshal(stringer.String()) 99 | } else { 100 | b, err = json.Marshal(val) 101 | } 102 | 103 | if err != nil { 104 | InternalLog.Error("Could not json.Marshal value: ", "formatter", "JSONFormatter", "err", err.Error()) 105 | if s, ok := val.(string); ok { 106 | b, err = json.Marshal(s) 107 | } else if s, ok := val.(fmt.Stringer); ok { 108 | b, err = json.Marshal(s.String()) 109 | } else { 110 | b, err = json.Marshal(fmt.Sprintf("%#v", val)) 111 | } 112 | 113 | if err != nil { 114 | // should never get here, but JSONFormatter should never panic 115 | msg := "Could not Sprintf value" 116 | InternalLog.Error(msg) 117 | buf.WriteString(`"` + msg + `"`) 118 | return 119 | } 120 | } 121 | buf.Write(b) 122 | } 123 | } 124 | 125 | func (jf *JSONFormatter) set(buf bufferWriter, key string, val interface{}) { 126 | // WARNING: assumes this is not first key 127 | buf.WriteString(`, "`) 128 | buf.WriteString(key) 129 | buf.WriteString(`":`) 130 | jf.appendValue(buf, val) 131 | } 132 | 133 | // Format formats log entry as JSON. 134 | func (jf *JSONFormatter) Format(writer io.Writer, level int, msg string, args []interface{}) { 135 | buf := pool.Get() 136 | defer pool.Put(buf) 137 | 138 | const lead = `", "` 139 | const colon = `":"` 140 | 141 | buf.WriteString(`{"`) 142 | buf.WriteString(KeyMap.Time) 143 | buf.WriteString(`":"`) 144 | buf.WriteString(time.Now().Format(timeFormat)) 145 | 146 | buf.WriteString(`", "`) 147 | buf.WriteString(KeyMap.PID) 148 | buf.WriteString(`":"`) 149 | buf.WriteString(pidStr) 150 | 151 | buf.WriteString(`", "`) 152 | buf.WriteString(KeyMap.Level) 153 | buf.WriteString(`":"`) 154 | buf.WriteString(LevelMap[level]) 155 | 156 | buf.WriteString(`", "`) 157 | buf.WriteString(KeyMap.Name) 158 | buf.WriteString(`":"`) 159 | buf.WriteString(jf.name) 160 | 161 | buf.WriteString(`", "`) 162 | buf.WriteString(KeyMap.Message) 163 | buf.WriteString(`":`) 164 | jf.appendValue(buf, msg) 165 | 166 | var lenArgs = len(args) 167 | if lenArgs > 0 { 168 | if lenArgs == 1 { 169 | jf.set(buf, singleArgKey, args[0]) 170 | } else if lenArgs%2 == 0 { 171 | for i := 0; i < lenArgs; i += 2 { 172 | if key, ok := args[i].(string); ok { 173 | if key == "" { 174 | // show key is invalid 175 | jf.set(buf, badKeyAtIndex(i), args[i+1]) 176 | } else { 177 | jf.set(buf, key, args[i+1]) 178 | } 179 | } else { 180 | // show key is invalid 181 | jf.set(buf, badKeyAtIndex(i), args[i+1]) 182 | } 183 | } 184 | } else { 185 | jf.set(buf, warnImbalancedKey, args) 186 | } 187 | } 188 | buf.WriteString("}\n") 189 | buf.WriteTo(writer) 190 | } 191 | 192 | // LogEntry returns the JSON log entry object built by Format(). Used by 193 | // HappyDevFormatter to ensure any data logged while developing properly 194 | // logs in production. 195 | func (jf *JSONFormatter) LogEntry(level int, msg string, args []interface{}) map[string]interface{} { 196 | buf := pool.Get() 197 | defer pool.Put(buf) 198 | jf.Format(buf, level, msg, args) 199 | var entry map[string]interface{} 200 | err := json.Unmarshal(buf.Bytes(), &entry) 201 | if err != nil { 202 | panic("Unable to unmarhsal entry from JSONFormatter: " + err.Error() + " \"" + string(buf.Bytes()) + "\"") 203 | } 204 | return entry 205 | } 206 | -------------------------------------------------------------------------------- /v1/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | /* 4 | http://en.wikipedia.org/wiki/Syslog 5 | 6 | Code Severity Keyword 7 | 0 Emergency emerg (panic) System is unusable. 8 | 9 | A "panic" condition usually affecting multiple apps/servers/sites. At this 10 | level it would usually notify all tech staff on call. 11 | 12 | 1 Alert alert Action must be taken immediately. 13 | 14 | Should be corrected immediately, therefore notify staff who can fix the 15 | problem. An example would be the loss of a primary ISP connection. 16 | 17 | 2 Critical crit Critical conditions. 18 | 19 | Should be corrected immediately, but indicates failure in a secondary 20 | system, an example is a loss of a backup ISP connection. 21 | 22 | 3 Error err (error) Error conditions. 23 | 24 | Non-urgent failures, these should be relayed to developers or admins; each 25 | item must be resolved within a given time. 26 | 27 | 4 Warning warning (warn) Warning conditions. 28 | 29 | Warning messages, not an error, but indication that an error will occur if 30 | action is not taken, e.g. file system 85% full - each item must be resolved 31 | within a given time. 32 | 33 | 5 Notice notice Normal but significant condition. 34 | 35 | Events that are unusual but not error conditions - might be summarized in 36 | an email to developers or admins to spot potential problems - no immediate 37 | action required. 38 | 39 | 6 Informational info Informational messages. 40 | 41 | Normal operational messages - may be harvested for reporting, measuring 42 | throughput, etc. - no action required. 43 | 44 | 7 Debug debug Debug-level messages. 45 | 46 | Info useful to developers for debugging the application, not useful during operations. 47 | */ 48 | 49 | const ( 50 | // LevelEnv chooses level from LOGXI environment variable or defaults 51 | // to LevelInfo 52 | LevelEnv = -10000 53 | 54 | // LevelOff means logging is disabled for logger. This should always 55 | // be first 56 | LevelOff = -1000 57 | 58 | // LevelEmergency is usually 0 but that is also the "zero" value 59 | // for Go, which means whenever we do any lookup in string -> int 60 | // map 0 is returned (not good). 61 | LevelEmergency = -1 62 | 63 | // LevelAlert means action must be taken immediately. 64 | LevelAlert = 1 65 | 66 | // LevelFatal means it should be corrected immediately, eg cannot connect to database. 67 | LevelFatal = 2 68 | 69 | // LevelCritical is alias for LevelFatal 70 | LevelCritical = 2 71 | 72 | // LevelError is a non-urgen failure to notify devlopers or admins 73 | LevelError = 3 74 | 75 | // LevelWarn indiates an error will occur if action is not taken, eg file system 85% full 76 | LevelWarn = 4 77 | 78 | // LevelNotice is normal but significant condition. 79 | LevelNotice = 5 80 | 81 | // LevelInfo is info level 82 | LevelInfo = 6 83 | 84 | // LevelDebug is debug level 85 | LevelDebug = 7 86 | 87 | // LevelTrace is trace level and displays file and line in terminal 88 | LevelTrace = 10 89 | 90 | // LevelAll is all levels 91 | LevelAll = 1000 92 | ) 93 | 94 | // FormatHappy uses HappyDevFormatter 95 | const FormatHappy = "happy" 96 | 97 | // FormatText uses TextFormatter 98 | const FormatText = "text" 99 | 100 | // FormatJSON uses JSONFormatter 101 | const FormatJSON = "JSON" 102 | 103 | // FormatEnv selects formatter based on LOGXI_FORMAT environment variable 104 | const FormatEnv = "" 105 | 106 | // LevelMap maps int enums to string level. 107 | var LevelMap = map[int]string{ 108 | LevelFatal: "FTL", 109 | LevelError: "ERR", 110 | LevelWarn: "WRN", 111 | LevelInfo: "INF", 112 | LevelDebug: "DBG", 113 | LevelTrace: "TRC", 114 | } 115 | 116 | // LevelMap maps int enums to string level. 117 | var LevelAtoi = map[string]int{ 118 | "OFF": LevelOff, 119 | "FTL": LevelFatal, 120 | "ERR": LevelError, 121 | "WRN": LevelWarn, 122 | "INF": LevelInfo, 123 | "DBG": LevelDebug, 124 | "TRC": LevelTrace, 125 | "ALL": LevelAll, 126 | 127 | "off": LevelOff, 128 | "fatal": LevelFatal, 129 | "error": LevelError, 130 | "warn": LevelWarn, 131 | "info": LevelInfo, 132 | "debug": LevelDebug, 133 | "trace": LevelTrace, 134 | "all": LevelAll, 135 | } 136 | 137 | // Logger is the interface for logging. 138 | type Logger interface { 139 | Trace(msg string, args ...interface{}) 140 | Debug(msg string, args ...interface{}) 141 | Info(msg string, args ...interface{}) 142 | Warn(msg string, args ...interface{}) error 143 | Error(msg string, args ...interface{}) error 144 | Fatal(msg string, args ...interface{}) 145 | Log(level int, msg string, args []interface{}) 146 | 147 | SetLevel(int) 148 | IsTrace() bool 149 | IsDebug() bool 150 | IsInfo() bool 151 | IsWarn() bool 152 | // Error, Fatal not needed, those SHOULD always be logged 153 | } 154 | -------------------------------------------------------------------------------- /v1/logger_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "os" 8 | "regexp" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func processEnv() { 16 | ProcessEnv(readFromEnviron()) 17 | } 18 | 19 | func testResetEnv() { 20 | disableColors = false 21 | testBuf.Reset() 22 | os.Clearenv() 23 | processEnv() 24 | InternalLog = testInternalLog 25 | } 26 | 27 | func TestEnvLOGXI(t *testing.T) { 28 | assert := assert.New(t) 29 | 30 | os.Setenv("LOGXI", "") 31 | processEnv() 32 | assert.Equal(LevelWarn, logxiNameLevelMap["*"], "Unset LOGXI defaults to *:WRN with TTY") 33 | 34 | // default all to ERR 35 | os.Setenv("LOGXI", "*=ERR") 36 | processEnv() 37 | level := getLogLevel("mylog") 38 | assert.Equal(LevelError, level) 39 | level = getLogLevel("mylog2") 40 | assert.Equal(LevelError, level) 41 | 42 | // unrecognized defaults to LevelDebug on TTY 43 | os.Setenv("LOGXI", "mylog=badlevel") 44 | processEnv() 45 | level = getLogLevel("mylog") 46 | assert.Equal(LevelWarn, level) 47 | 48 | // wildcard should not override exact match 49 | os.Setenv("LOGXI", "*=WRN,mylog=ERR,other=OFF") 50 | processEnv() 51 | level = getLogLevel("mylog") 52 | assert.Equal(LevelError, level) 53 | level = getLogLevel("other") 54 | assert.Equal(LevelOff, level) 55 | 56 | // wildcard pattern should match 57 | os.Setenv("LOGXI", "*log=ERR") 58 | processEnv() 59 | level = getLogLevel("mylog") 60 | assert.Equal(LevelError, level, "wildcat prefix should match") 61 | 62 | os.Setenv("LOGXI", "myx*=ERR") 63 | processEnv() 64 | level = getLogLevel("mylog") 65 | assert.Equal(LevelError, level, "no match should return LevelError") 66 | 67 | os.Setenv("LOGXI", "myl*,-foo") 68 | processEnv() 69 | level = getLogLevel("mylog") 70 | assert.Equal(LevelAll, level) 71 | level = getLogLevel("foo") 72 | assert.Equal(LevelOff, level) 73 | } 74 | 75 | func TestEnvLOGXI_FORMAT(t *testing.T) { 76 | assert := assert.New(t) 77 | oldIsTerminal := isTerminal 78 | 79 | os.Setenv("LOGXI_FORMAT", "") 80 | setDefaults(true) 81 | processEnv() 82 | assert.Equal(FormatHappy, logxiFormat, "terminal defaults to FormatHappy") 83 | setDefaults(false) 84 | processEnv() 85 | assert.Equal(FormatJSON, logxiFormat, "non terminal defaults to FormatJSON") 86 | 87 | os.Setenv("LOGXI_FORMAT", "JSON") 88 | processEnv() 89 | assert.Equal(FormatJSON, logxiFormat) 90 | 91 | os.Setenv("LOGXI_FORMAT", "json") 92 | setDefaults(true) 93 | processEnv() 94 | assert.Equal(FormatHappy, logxiFormat, "Mismatches defaults to FormatHappy") 95 | setDefaults(false) 96 | processEnv() 97 | assert.Equal(FormatJSON, logxiFormat, "Mismatches defaults to FormatJSON non terminal") 98 | 99 | isTerminal = oldIsTerminal 100 | setDefaults(isTerminal) 101 | } 102 | 103 | func TestEnvLOGXI_COLORS(t *testing.T) { 104 | oldIsTerminal := isTerminal 105 | 106 | os.Setenv("LOGXI_COLORS", "*=off") 107 | setDefaults(true) 108 | processEnv() 109 | 110 | var buf bytes.Buffer 111 | l := NewLogger3(&buf, "telc", NewHappyDevFormatter("logxi-colors")) 112 | l.SetLevel(LevelDebug) 113 | l.Info("info") 114 | 115 | r := regexp.MustCompile(`^\d{2}:\d{2}:\d{2}\.\d{6} INF logxi-colors info`) 116 | assert.True(t, r.Match(buf.Bytes())) 117 | 118 | setDefaults(true) 119 | 120 | isTerminal = oldIsTerminal 121 | setDefaults(isTerminal) 122 | } 123 | 124 | func TestComplexKeys(t *testing.T) { 125 | testResetEnv() 126 | var buf bytes.Buffer 127 | l := NewLogger(&buf, "bench") 128 | assert.Panics(t, func() { 129 | l.Error("complex", "foo\n", 1) 130 | }) 131 | 132 | assert.Panics(t, func() { 133 | l.Error("complex", "foo\"s", 1) 134 | }) 135 | 136 | l.Error("apos is ok", "foo's", 1) 137 | } 138 | 139 | func TestJSON(t *testing.T) { 140 | testResetEnv() 141 | var buf bytes.Buffer 142 | l := NewLogger3(&buf, "bench", NewJSONFormatter("bench")) 143 | l.SetLevel(LevelDebug) 144 | l.Error("hello", "foo", "bar") 145 | 146 | var obj map[string]interface{} 147 | err := json.Unmarshal(buf.Bytes(), &obj) 148 | assert.NoError(t, err) 149 | assert.Equal(t, "bar", obj["foo"].(string)) 150 | assert.Equal(t, "hello", obj[KeyMap.Message].(string)) 151 | } 152 | 153 | func TestJSONImbalanced(t *testing.T) { 154 | testResetEnv() 155 | var buf bytes.Buffer 156 | l := NewLogger3(&buf, "bench", NewJSONFormatter("bench")) 157 | l.SetLevel(LevelDebug) 158 | l.Error("hello", "foo", "bar", "bah") 159 | 160 | var obj map[string]interface{} 161 | err := json.Unmarshal(buf.Bytes(), &obj) 162 | assert.NoError(t, err) 163 | assert.Exactly(t, []interface{}{"foo", "bar", "bah"}, obj[warnImbalancedKey]) 164 | assert.Equal(t, "hello", obj[KeyMap.Message].(string)) 165 | } 166 | 167 | func TestJSONNoArgs(t *testing.T) { 168 | testResetEnv() 169 | var buf bytes.Buffer 170 | l := NewLogger3(&buf, "bench", NewJSONFormatter("bench")) 171 | l.SetLevel(LevelDebug) 172 | l.Error("hello") 173 | 174 | var obj map[string]interface{} 175 | err := json.Unmarshal(buf.Bytes(), &obj) 176 | assert.NoError(t, err) 177 | assert.Equal(t, "hello", obj[KeyMap.Message].(string)) 178 | } 179 | 180 | func TestJSONNested(t *testing.T) { 181 | testResetEnv() 182 | var buf bytes.Buffer 183 | l := NewLogger3(&buf, "bench", NewJSONFormatter("bench")) 184 | l.SetLevel(LevelDebug) 185 | l.Error("hello", "obj", map[string]string{"fruit": "apple"}) 186 | 187 | var obj map[string]interface{} 188 | err := json.Unmarshal(buf.Bytes(), &obj) 189 | assert.NoError(t, err) 190 | assert.Equal(t, "hello", obj[KeyMap.Message].(string)) 191 | o := obj["obj"] 192 | assert.Equal(t, "apple", o.(map[string]interface{})["fruit"].(string)) 193 | } 194 | 195 | func TestJSONEscapeSequences(t *testing.T) { 196 | testResetEnv() 197 | var buf bytes.Buffer 198 | l := NewLogger3(&buf, "bench", NewJSONFormatter("bench")) 199 | l.SetLevel(LevelDebug) 200 | esc := "I said, \"a's \\ \\\b\f\n\r\t\x1a\"你好'; DELETE FROM people" 201 | 202 | var obj map[string]interface{} 203 | // test as message 204 | l.Error(esc) 205 | err := json.Unmarshal(buf.Bytes(), &obj) 206 | assert.NoError(t, err) 207 | assert.Equal(t, esc, obj[KeyMap.Message].(string)) 208 | 209 | // test as key 210 | buf.Reset() 211 | key := "你好" 212 | l.Error("as key", key, "esc") 213 | err = json.Unmarshal(buf.Bytes(), &obj) 214 | assert.NoError(t, err) 215 | assert.Equal(t, "as key", obj[KeyMap.Message].(string)) 216 | assert.Equal(t, "esc", obj[key].(string)) 217 | } 218 | 219 | func TestKeyNotString(t *testing.T) { 220 | testResetEnv() 221 | var buf bytes.Buffer 222 | l := NewLogger3(&buf, "badkey", NewHappyDevFormatter("badkey")) 223 | l.SetLevel(LevelDebug) 224 | l.Debug("foo", 1) 225 | assert.Panics(t, func() { 226 | l.Debug("reserved key", "_t", "trying to use time") 227 | }) 228 | } 229 | 230 | func TestWarningErrorContext(t *testing.T) { 231 | testResetEnv() 232 | var buf bytes.Buffer 233 | l := NewLogger3(&buf, "wrnerr", NewHappyDevFormatter("wrnerr")) 234 | l.Warn("no keys") 235 | l.Warn("has eys", "key1", 2) 236 | l.Error("no keys") 237 | l.Error("has keys", "key1", 2) 238 | } 239 | 240 | func TestLevels(t *testing.T) { 241 | var buf bytes.Buffer 242 | l := NewLogger3(&buf, "bench", NewJSONFormatter("bench")) 243 | 244 | l.SetLevel(LevelFatal) 245 | assert.False(t, l.IsWarn()) 246 | assert.False(t, l.IsInfo()) 247 | assert.False(t, l.IsTrace()) 248 | assert.False(t, l.IsDebug()) 249 | 250 | l.SetLevel(LevelError) 251 | assert.False(t, l.IsWarn()) 252 | 253 | l.SetLevel(LevelWarn) 254 | assert.True(t, l.IsWarn()) 255 | assert.False(t, l.IsDebug()) 256 | 257 | l.SetLevel(LevelInfo) 258 | assert.True(t, l.IsInfo()) 259 | assert.True(t, l.IsWarn()) 260 | assert.False(t, l.IsDebug()) 261 | 262 | l.SetLevel(LevelDebug) 263 | assert.True(t, l.IsDebug()) 264 | assert.True(t, l.IsInfo()) 265 | assert.False(t, l.IsTrace()) 266 | 267 | l.SetLevel(LevelTrace) 268 | assert.True(t, l.IsTrace()) 269 | assert.True(t, l.IsDebug()) 270 | } 271 | 272 | func TestAllowSingleParam(t *testing.T) { 273 | var buf bytes.Buffer 274 | l := NewLogger3(&buf, "wrnerr", NewTextFormatter("wrnerr")) 275 | l.SetLevel(LevelDebug) 276 | l.Info("info", 1) 277 | assert.True(t, strings.HasSuffix(buf.String(), singleArgKey+": 1\n")) 278 | 279 | buf.Reset() 280 | l = NewLogger3(&buf, "wrnerr", NewHappyDevFormatter("wrnerr")) 281 | l.SetLevel(LevelDebug) 282 | l.Info("info", 1) 283 | assert.True(t, strings.HasSuffix(buf.String(), "_: \x1b[0m1\n")) 284 | 285 | var obj map[string]interface{} 286 | buf.Reset() 287 | l = NewLogger3(&buf, "wrnerr", NewJSONFormatter("wrnerr")) 288 | l.SetLevel(LevelDebug) 289 | l.Info("info", 1) 290 | err := json.Unmarshal(buf.Bytes(), &obj) 291 | assert.NoError(t, err) 292 | assert.Equal(t, float64(1), obj["_"]) 293 | } 294 | 295 | func TestErrorOnWarn(t *testing.T) { 296 | testResetEnv() 297 | // os.Setenv("LOGXI_FORMAT", "context=2") 298 | // processEnv() 299 | var buf bytes.Buffer 300 | l := NewLogger3(&buf, "wrnerr", NewHappyDevFormatter("wrnerr")) 301 | l.SetLevel(LevelWarn) 302 | 303 | ErrorDummy := errors.New("dummy error") 304 | 305 | err := l.Warn("warn with error", "err", ErrorDummy) 306 | assert.Error(t, err) 307 | assert.Equal(t, "dummy error", err.Error()) 308 | err = l.Warn("warn with no error", "one", 1) 309 | assert.NoError(t, err) 310 | //l.Error("error with err", "err", ErrorDummy) 311 | } 312 | 313 | type CheckStringer struct { 314 | s string 315 | } 316 | 317 | func (cs CheckStringer) String() string { 318 | return "bbb" 319 | } 320 | 321 | func TestStringer(t *testing.T) { 322 | f := CheckStringer{s: "aaa"} 323 | 324 | var buf bytes.Buffer 325 | l := NewLogger3(&buf, "cs1", NewTextFormatter("stringer-text")) 326 | l.SetLevel(LevelDebug) 327 | l.Info("info", "f", f) 328 | assert.True(t, strings.Contains(buf.String(), "bbb")) 329 | 330 | buf.Reset() 331 | l = NewLogger3(&buf, "cs2", NewHappyDevFormatter("stringer-happy")) 332 | l.SetLevel(LevelDebug) 333 | l.Info("info", "f", f) 334 | assert.True(t, strings.Contains(buf.String(), "bbb")) 335 | 336 | var obj map[string]interface{} 337 | buf.Reset() 338 | l = NewLogger3(&buf, "cs3", NewJSONFormatter("stringer-json")) 339 | l.SetLevel(LevelDebug) 340 | l.Info("info", "f", f) 341 | err := json.Unmarshal(buf.Bytes(), &obj) 342 | assert.NoError(t, err) 343 | assert.Equal(t, "bbb", obj["f"]) 344 | } 345 | 346 | // When log functions cast pointers to interface{}. 347 | // Say p is a pointer set to nil: 348 | // 349 | // interface{}(p) == nil // this is false 350 | // 351 | // Casting it to interface{} makes it trickier to test whether its nil. 352 | func TestStringerNullPointers(t *testing.T) { 353 | var f *CheckStringer 354 | var buf bytes.Buffer 355 | l := NewLogger3(&buf, "cs1", NewJSONFormatter("stringer-json")) 356 | l.SetLevel(LevelDebug) 357 | l.Info("info", "f", f) 358 | assert.Contains(t, buf.String(), "null") 359 | } -------------------------------------------------------------------------------- /v1/methods.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | // Trace logs a trace statement. On terminals file and line number are logged. 4 | func Trace(msg string, args ...interface{}) { 5 | DefaultLog.Trace(msg, args...) 6 | } 7 | 8 | // Debug logs a debug statement. 9 | func Debug(msg string, args ...interface{}) { 10 | DefaultLog.Debug(msg, args...) 11 | } 12 | 13 | // Info logs an info statement. 14 | func Info(msg string, args ...interface{}) { 15 | DefaultLog.Info(msg, args...) 16 | } 17 | 18 | // Warn logs a warning statement. On terminals it logs file and line number. 19 | func Warn(msg string, args ...interface{}) { 20 | DefaultLog.Warn(msg, args...) 21 | } 22 | 23 | // Error logs an error statement with callstack. 24 | func Error(msg string, args ...interface{}) { 25 | DefaultLog.Error(msg, args...) 26 | } 27 | 28 | // Fatal logs a fatal statement. 29 | func Fatal(msg string, args ...interface{}) { 30 | DefaultLog.Fatal(msg, args...) 31 | } 32 | 33 | // IsTrace determines if this logger logs a trace statement. 34 | func IsTrace() bool { 35 | return DefaultLog.IsTrace() 36 | } 37 | 38 | // IsDebug determines if this logger logs a debug statement. 39 | func IsDebug() bool { 40 | return DefaultLog.IsDebug() 41 | } 42 | 43 | // IsInfo determines if this logger logs an info statement. 44 | func IsInfo() bool { 45 | return DefaultLog.IsInfo() 46 | } 47 | 48 | // IsWarn determines if this logger logs a warning statement. 49 | func IsWarn() bool { 50 | return DefaultLog.IsWarn() 51 | } 52 | -------------------------------------------------------------------------------- /v1/nullLogger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | // NullLog is a noop logger. Think of it as /dev/null. 4 | var NullLog = &NullLogger{} 5 | 6 | // NullLogger is the default logger for this package. 7 | type NullLogger struct{} 8 | 9 | // Trace logs a debug entry. 10 | func (l *NullLogger) Trace(msg string, args ...interface{}) { 11 | } 12 | 13 | // Debug logs a debug entry. 14 | func (l *NullLogger) Debug(msg string, args ...interface{}) { 15 | } 16 | 17 | // Info logs an info entry. 18 | func (l *NullLogger) Info(msg string, args ...interface{}) { 19 | } 20 | 21 | // Warn logs a warn entry. 22 | func (l *NullLogger) Warn(msg string, args ...interface{}) error { 23 | return nil 24 | } 25 | 26 | // Error logs an error entry. 27 | func (l *NullLogger) Error(msg string, args ...interface{}) error { 28 | return nil 29 | } 30 | 31 | // Fatal logs a fatal entry then panics. 32 | func (l *NullLogger) Fatal(msg string, args ...interface{}) { 33 | panic("exit due to fatal error") 34 | } 35 | 36 | // Log logs a leveled entry. 37 | func (l *NullLogger) Log(level int, msg string, args []interface{}) { 38 | } 39 | 40 | // IsTrace determines if this logger logs a trace statement. 41 | func (l *NullLogger) IsTrace() bool { 42 | return false 43 | } 44 | 45 | // IsDebug determines if this logger logs a debug statement. 46 | func (l *NullLogger) IsDebug() bool { 47 | return false 48 | } 49 | 50 | // IsInfo determines if this logger logs an info statement. 51 | func (l *NullLogger) IsInfo() bool { 52 | return false 53 | } 54 | 55 | // IsWarn determines if this logger logs a warning statement. 56 | func (l *NullLogger) IsWarn() bool { 57 | return false 58 | } 59 | 60 | // SetLevel sets the level of this logger. 61 | func (l *NullLogger) SetLevel(level int) { 62 | } 63 | 64 | // SetFormatter set the formatter for this logger. 65 | func (l *NullLogger) SetFormatter(formatter Formatter) { 66 | } 67 | -------------------------------------------------------------------------------- /v1/pool.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | type BufferPool struct { 9 | sync.Pool 10 | } 11 | 12 | func NewBufferPool() *BufferPool { 13 | return &BufferPool{ 14 | Pool: sync.Pool{New: func() interface{} { 15 | b := bytes.NewBuffer(make([]byte, 128)) 16 | b.Reset() 17 | return b 18 | }}, 19 | } 20 | } 21 | 22 | func (bp *BufferPool) Get() *bytes.Buffer { 23 | return bp.Pool.Get().(*bytes.Buffer) 24 | } 25 | 26 | func (bp *BufferPool) Put(b *bytes.Buffer) { 27 | b.Reset() 28 | bp.Pool.Put(b) 29 | } 30 | -------------------------------------------------------------------------------- /v1/textFormatter.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "runtime/debug" 7 | "time" 8 | ) 9 | 10 | // Formatter records log entries. 11 | type Formatter interface { 12 | Format(writer io.Writer, level int, msg string, args []interface{}) 13 | } 14 | 15 | // TextFormatter is the default recorder used if one is unspecified when 16 | // creating a new Logger. 17 | type TextFormatter struct { 18 | name string 19 | itoaLevelMap map[int]string 20 | timeLabel string 21 | } 22 | 23 | // NewTextFormatter returns a new instance of TextFormatter. SetName 24 | // must be called befored using it. 25 | func NewTextFormatter(name string) *TextFormatter { 26 | timeLabel := KeyMap.Time + AssignmentChar 27 | levelLabel := Separator + KeyMap.Level + AssignmentChar 28 | messageLabel := Separator + KeyMap.Message + AssignmentChar 29 | nameLabel := Separator + KeyMap.Name + AssignmentChar 30 | pidLabel := Separator + KeyMap.PID + AssignmentChar 31 | 32 | var buildKV = func(level string) string { 33 | buf := pool.Get() 34 | defer pool.Put(buf) 35 | 36 | buf.WriteString(pidLabel) 37 | buf.WriteString(pidStr) 38 | 39 | //buf.WriteString(Separator) 40 | buf.WriteString(nameLabel) 41 | buf.WriteString(name) 42 | 43 | //buf.WriteString(Separator) 44 | buf.WriteString(levelLabel) 45 | buf.WriteString(level) 46 | 47 | //buf.WriteString(Separator) 48 | buf.WriteString(messageLabel) 49 | 50 | return buf.String() 51 | } 52 | itoaLevelMap := map[int]string{ 53 | LevelDebug: buildKV(LevelMap[LevelDebug]), 54 | LevelWarn: buildKV(LevelMap[LevelWarn]), 55 | LevelInfo: buildKV(LevelMap[LevelInfo]), 56 | LevelError: buildKV(LevelMap[LevelError]), 57 | LevelFatal: buildKV(LevelMap[LevelFatal]), 58 | } 59 | return &TextFormatter{itoaLevelMap: itoaLevelMap, name: name, timeLabel: timeLabel} 60 | } 61 | 62 | func (tf *TextFormatter) set(buf bufferWriter, key string, val interface{}) { 63 | buf.WriteString(Separator) 64 | buf.WriteString(key) 65 | buf.WriteString(AssignmentChar) 66 | if err, ok := val.(error); ok { 67 | buf.WriteString(err.Error()) 68 | buf.WriteRune('\n') 69 | buf.WriteString(string(debug.Stack())) 70 | return 71 | } 72 | buf.WriteString(fmt.Sprintf("%v", val)) 73 | } 74 | 75 | // Format records a log entry. 76 | func (tf *TextFormatter) Format(writer io.Writer, level int, msg string, args []interface{}) { 77 | buf := pool.Get() 78 | defer pool.Put(buf) 79 | buf.WriteString(tf.timeLabel) 80 | buf.WriteString(time.Now().Format(timeFormat)) 81 | buf.WriteString(tf.itoaLevelMap[level]) 82 | buf.WriteString(msg) 83 | var lenArgs = len(args) 84 | if lenArgs > 0 { 85 | if lenArgs == 1 { 86 | tf.set(buf, singleArgKey, args[0]) 87 | } else if lenArgs%2 == 0 { 88 | for i := 0; i < lenArgs; i += 2 { 89 | if key, ok := args[i].(string); ok { 90 | if key == "" { 91 | // show key is invalid 92 | tf.set(buf, badKeyAtIndex(i), args[i+1]) 93 | } else { 94 | tf.set(buf, key, args[i+1]) 95 | } 96 | } else { 97 | // show key is invalid 98 | tf.set(buf, badKeyAtIndex(i), args[i+1]) 99 | } 100 | } 101 | } else { 102 | tf.set(buf, warnImbalancedKey, args) 103 | } 104 | } 105 | buf.WriteRune('\n') 106 | buf.WriteTo(writer) 107 | } 108 | -------------------------------------------------------------------------------- /v1/util.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | ) 7 | 8 | func expandTabs(s string, tabLen int) string { 9 | if s == "" { 10 | return s 11 | } 12 | parts := strings.Split(s, "\t") 13 | buf := pool.Get() 14 | defer pool.Put(buf) 15 | for _, part := range parts { 16 | buf.WriteString(part) 17 | buf.WriteString(strings.Repeat(" ", tabLen-len(part)%tabLen)) 18 | } 19 | return buf.String() 20 | } 21 | 22 | func maxInt(a, b int) int { 23 | if a > b { 24 | return a 25 | } 26 | return b 27 | } 28 | func minInt(a, b int) int { 29 | if a < b { 30 | return a 31 | } 32 | return b 33 | } 34 | 35 | func indexOfNonSpace(s string) int { 36 | if s == "" { 37 | return -1 38 | } 39 | for i, r := range s { 40 | if r != ' ' { 41 | return i 42 | } 43 | } 44 | return -1 45 | } 46 | 47 | var inLogxiPath = filepath.Join("mgutz", "logxi", "v"+strings.Split(Version, ".")[0]) 48 | 49 | func isLogxiCode(filename string) bool { 50 | // need to see errors in tests 51 | return strings.HasSuffix(filepath.Dir(filename), inLogxiPath) && 52 | !strings.HasSuffix(filename, "_test.go") 53 | } 54 | -------------------------------------------------------------------------------- /v1/version.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | // Version is the version of this package 4 | const Version = "1.0.0-pre" 5 | --------------------------------------------------------------------------------