├── go.mod ├── flow.dia ├── flow.png ├── kiwi-vs-other.png ├── flomar-kiwi-bird-300px.png ├── cmd ├── example-minimal │ └── main.go ├── README.md ├── example-sink-start-stop │ └── main.go ├── example-with-context │ └── main.go └── example-several-outputs │ └── main.go ├── .deepsource.toml ├── .gitignore ├── .travis.yml ├── where ├── README.md ├── runtime.go └── where_test.go ├── LICENSE ├── TODO.org ├── timestamp ├── timestamp.go └── timestamp_test.go ├── obsoleted.go ├── doc.go ├── global-logger.go ├── filter.go ├── loggers_benchmark_json_test.go ├── strict └── strict-pairs.go ├── global-context.go ├── context.go ├── formatter.go ├── context_test.go ├── loggers_benchmark_logfmt_test.go ├── convertor.go ├── level ├── global-levels.go ├── levels.go └── levels_test.go ├── logger.go ├── convertor_test.go ├── logger_json_test.go ├── logger_test.go ├── race_test.go ├── sink.go ├── sink_test.go ├── global-logger_test.go ├── README.md └── kiwi-vs-other.graphml /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafov/kiwi 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /flow.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafov/kiwi/HEAD/flow.dia -------------------------------------------------------------------------------- /flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafov/kiwi/HEAD/flow.png -------------------------------------------------------------------------------- /kiwi-vs-other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafov/kiwi/HEAD/kiwi-vs-other.png -------------------------------------------------------------------------------- /flomar-kiwi-bird-300px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafov/kiwi/HEAD/flomar-kiwi-bird-300px.png -------------------------------------------------------------------------------- /cmd/example-minimal/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/grafov/kiwi" 7 | ) 8 | 9 | func main() { 10 | kiwi.SinkTo(os.Stdout, kiwi.AsLogfmt()).Start() 11 | kiwi.Log("key1", "text value", "key2", 123) 12 | } 13 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = [ 4 | "*_test.go" 5 | ] 6 | 7 | exclude_patterns = [ 8 | "vendor/**" 9 | ] 10 | 11 | [[analyzers]] 12 | name = "go" 13 | enabled = true 14 | 15 | [analyzers.meta] 16 | import_path = "github.com/grafov/kiwi" 17 | -------------------------------------------------------------------------------- /cmd/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | * [example-minimal](example-minimal) — import the package and log the sample record by global logger 4 | * [example-sink-start-stop](example-sink-start-stop) — create logger instance, show how to start and stop output 5 | * [example-several-outputs](example-several-outputs) — show how to filter keys and values for redirecting log records between sinks 6 | * [example-with-context](example-with-context) — demonstrate the context usage 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Ignore wikipages that keeped locally inside the project repo 27 | /kiwi.wiki -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | # Versions of go that are explicitly supported. 4 | go: 5 | - 1.15 6 | - tip 7 | 8 | # Required for coverage. 9 | before_install: 10 | - go get golang.org/x/tools/cmd/cover 11 | - go get github.com/mattn/goveralls 12 | 13 | script: 14 | - go build -a -v ./... 15 | - diff <(gofmt -d .) <("") 16 | - go test -v -covermode=count -coverprofile=coverage.out 17 | - $GOPATH/bin/goveralls -coverprofile=coverage.out -service=travis-ci 18 | -------------------------------------------------------------------------------- /where/README.md: -------------------------------------------------------------------------------- 1 | # Runtime package for Kiwi log (experimental) 2 | 3 | The package adds runtime information (package name, function, line 4 | number) as the keys and the values to the logger context. 5 | 6 | ```go 7 | import ( 8 | "os" 9 | 10 | "github.com/grafov/kiwi" 11 | "github.com/grafov/kiwi/where" 12 | ) 13 | 14 | func main() { 15 | kiwi.SinkTo(os.Stdout, kiwi.AsLogfmt()).Start() 16 | 17 | kiwi.With(where.What(where.File | where.Line | where.Func)) 18 | kiwi.Log("key", "value") 19 | } 20 | ``` 21 | 22 | The result log record will be like that: 23 | 24 | lineno=11 file="path/to/main.go" function="main.main" key="value" 25 | -------------------------------------------------------------------------------- /cmd/example-sink-start-stop/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/grafov/kiwi" 7 | ) 8 | 9 | func main() { 10 | // Bind a new logger to a variable. You may create any number of 11 | // loggers. 12 | log := kiwi.New() 13 | 14 | // For starting write log records to some writer output should be 15 | // initialized. 16 | output := kiwi.SinkTo(os.Stdout, kiwi.AsLogfmt()).Start() 17 | 18 | log.Add("sample-record", 1, "key", "value") 19 | log.Log() 20 | 21 | // The most of the logger and the output operations support 22 | // chaining. 23 | log.Add("sample-record", 2, "key", "value", "key2", 123).Log() 24 | 25 | // On pause output will drop any incoming records. 26 | output.Stop() 27 | log.Add("this record will be dropped because single output we declared is on pause") 28 | output.Start() 29 | 30 | // The output will be automatically closed on application exit but 31 | // you can explicitly close it and free some memory. 32 | output.Close() 33 | } 34 | -------------------------------------------------------------------------------- /cmd/example-with-context/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/grafov/kiwi" 8 | ) 9 | 10 | func main() { 11 | // Bind a new logger to a variable. You may create any number of loggers. 12 | log := kiwi.New() 13 | 14 | // For starting write log records to some writer output should be initialized. 15 | out := kiwi.SinkTo(os.Stdout, kiwi.AsLogfmt()).Start() 16 | 17 | // setup context of the logger 18 | log.With("userID", 1000, "host", "local", "startedAt", time.Now()) 19 | 20 | // This record will be supplemented by startedAt value of time.Now().String() 21 | log.Add("sample", 1).Log() 22 | 23 | // This record also will be supplemented by the same value of the time. 24 | // Because context value evalueted when it was added by log.With(). 25 | log.Add("sample", 2).Log() 26 | 27 | // You can provide deferred evaluation of context or log values if you add them wrapped 28 | // with func() interface{}, where interface should be one of scalar golang types. 29 | log.With("currentTime", func() string { return time.Now().String() }) 30 | 31 | // These records will be output each its own currentTime value because currentTime will 32 | // be evaluated on each Log() call. 33 | log.Log("sample", 3) 34 | log.Log("sample", 4) 35 | out.Flush() 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Alexander I.Grafov aka Axel 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of kvlog nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /TODO.org: -------------------------------------------------------------------------------- 1 | #+TODO: WIP(s) STOPPED(p) | DONE(d) CANCELED(c@) 2 | * For realization 3 | ** DONE check that loggers works ok without any output 4 | случай неправильного порядка инита 5 | или закрытие всех имеющихся выводов 6 | ** DONE check artifical output names, use writers for distinction 7 | переименовать getoutput в getwriter? 8 | ** DONE custom formatters 9 | ** DONE add encoding.TextMarshaller 10 | ** DONE fix odd args logic: odd key showed as is without a value 11 | ** DONE add time.Time into convertors 12 | ** DONE move benchmarks to a separate package 13 | ** DONE global logger :roadmap: 14 | ** STOPPED mode: drop duplicate records in an output 15 | ** DONE start any sink paused 16 | It will help set filters before the sink got a lot of log messages and passes them by default. 17 | ** DONE clarify logic of sanitation for quoted keys and quoted values :bug: 18 | ** DONE add tests for output filters :roadmap: 19 | ** DONE clarify logic of odd keys 20 | maybe treat them as values? 21 | ** DONE ability to use custom filters :roadmap: 22 | ** WIP add tests for logger add/log for different input types 23 | ** WIP research solutions for multiline logging especially for go traces in other loggers 24 | ** add tests for formatters 25 | ** add tests for logger context 26 | ** add tests for logger timestamps 27 | ** move examples from README to example_ functions then add single short usage example to README 28 | ** colour format for the console (formatter or decorator) :roadmap: 29 | ** log write throttling mode :roadmap: 30 | ** demo application 31 | ** add more predefined filters for stdlib types (string, time.Time) :roadmap: 32 | * Ideas 33 | ** move typed helpers into a separated package or 34 | ** remove typed helpers - it seems as preferable way as it make API shorter 35 | Test results shows not many benefit from them. Need more tests. 36 | ** decorators for formatters 37 | 38 | -------------------------------------------------------------------------------- /cmd/example-several-outputs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/grafov/kiwi" 7 | ) 8 | 9 | func main() { 10 | // Bind a new logger to a variable. You may create any number of loggers. 11 | log := kiwi.New() 12 | 13 | tmpFile, _ := os.Create("/tmp/something-completely-different.log") 14 | 15 | // You can set arbitrary number of outputs. 16 | // But they will remain unused until you explicitly start them with Start(). 17 | info := kiwi.SinkTo(os.Stdout, kiwi.AsLogfmt()) 18 | errors := kiwi.SinkTo(os.Stderr, kiwi.AsLogfmt()) 19 | something := kiwi.SinkTo(tmpFile, kiwi.AsLogfmt()) 20 | 21 | // Each record by default will copied to all outputs. 22 | // But until you Start() any output the records will just dropped as the sample record below. 23 | log.Add("just something that will lost") 24 | 25 | // Each output allows filter out any records and write any other. 26 | // You specify filter for the keys (key filter). 27 | // Each of these keys should be presented in the record. 28 | errors.HasKey("error", "msg") 29 | // The filter may take into account key values. So only records with levels 30 | // ERROR and FATAL will be passed filter and written to stderr. 31 | errors.HasValue("level", "ERROR", "FATAL").Start() 32 | 33 | // Vice versa you can filter out some keys. 34 | info.HasNotKey("error") 35 | // And define another set of key-val pairs for distinguish outputs. 36 | info.HasValue("level", "INFO", "WARNING").Start() 37 | 38 | // It will output all records from outputs above if they have key "something". 39 | // So you can duplicate some records to several log files based on some criteria. 40 | something.HasKey("something").Start() 41 | 42 | // So if you not define any clauses (HasKey/HasNotKey/HasValue/WithoutValues) 43 | // then all records will copied to an output. 44 | 45 | // Let's go! 46 | log.Add("level", "INFO", "sample-record", 1, "key", "value") 47 | log.Add("level", "INFO", "sample-record", 2, "something").Log() 48 | log.Add("level", "ERROR", "msg", "Error description.").Log() 49 | log.Add("level", "FATAL").Log() 50 | 51 | // Until you call Log() records not copied to outputs. 52 | log.Log() 53 | } 54 | -------------------------------------------------------------------------------- /timestamp/timestamp.go: -------------------------------------------------------------------------------- 1 | package timestamp 2 | 3 | // Helper for using timestamp in the log output 4 | 5 | /* Copyright (c) 2016-2018, Alexander I.Grafov 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of kvlog nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ॐ तारे तुत्तारे तुरे स्व */ 34 | 35 | import ( 36 | "time" 37 | 38 | "github.com/grafov/kiwi" 39 | ) 40 | 41 | // DefaultKey defines the default key for the timestamp value. 42 | var DefaultKey = "at" 43 | 44 | // Set adds "timestamp" field to the context. 45 | func Set(format string) *kiwi.Pair { 46 | return &kiwi.Pair{ 47 | Key: DefaultKey, 48 | Val: "", 49 | Eval: func() string { return time.Now().Format(format) }, 50 | Type: kiwi.TimeVal, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /obsoleted.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | // Obsoleted functions that may be removed in the future. 4 | 5 | /* Copyright (c) 2016-2020, Alexander I.Grafov 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of kvlog nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ॐ तारे तुत्तारे तुरे स्व */ 34 | 35 | // FlushAll should wait for all the sinks to be flushed. It does 36 | // nothing currently. It has left for compatibility with old API. 37 | func FlushAll() { 38 | Log(InfoKey, "obsoleted", MessageKey, "FlushAll() is obsoleted, you should remove it from the code") 39 | } 40 | 41 | // Flush waits that all previously sent to the output records 42 | // worked. It does nothing currently. It has left for compatibility 43 | // with old API. 44 | func (s *Sink) Flush() *Sink { 45 | Log(InfoKey, "obsoleted", MessageKey, "sink.Flush() is obsoleted, you should remove it from the code") 46 | return s 47 | } 48 | -------------------------------------------------------------------------------- /timestamp/timestamp_test.go: -------------------------------------------------------------------------------- 1 | package timestamp 2 | 3 | /* 4 | Copyright (c) 2016, Alexander I.Grafov 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of kvlog nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ॐ तारे तुत्तारे तुरे स्व 33 | 34 | All tests consists of three parts: 35 | 36 | - arrange structures and initialize objects for use in tests 37 | - act on testing object 38 | - check and assert on results 39 | 40 | These parts separated by empty lines in each test function. 41 | */ 42 | 43 | import ( 44 | "bytes" 45 | "strings" 46 | "testing" 47 | 48 | "github.com/grafov/kiwi" 49 | ) 50 | 51 | // Test of log to the stopped sink. 52 | func TestSink_LogToStoppedSink_Logfmt(t *testing.T) { 53 | out := bytes.NewBufferString("") 54 | log := kiwi.New() 55 | sink := kiwi.SinkTo(out, kiwi.AsLogfmt()).Start() 56 | 57 | log.With(Set(kiwi.TimeLayout)) 58 | log.Log("key", "value") 59 | 60 | sink.Flush().Close() 61 | got := out.String() 62 | expected := `at=` 63 | if !strings.Contains(got, expected) { 64 | t.Logf("expected %s got %v", expected, got) 65 | t.Fail() 66 | } 67 | expected = `key="value"` 68 | if !strings.Contains(got, expected) { 69 | t.Logf("expected %s got %v", expected, got) 70 | t.Fail() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /where/runtime.go: -------------------------------------------------------------------------------- 1 | package where 2 | 3 | // Helper for adding runtime info to the logger context 4 | 5 | /* Copyright (c) 2016-2019, Alexander I.Grafov 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of kvlog nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ॐ तारे तुत्तारे तुरे स्व */ 34 | 35 | import ( 36 | "runtime" 37 | "strconv" 38 | "strings" 39 | 40 | "github.com/grafov/kiwi" 41 | ) 42 | 43 | const ( 44 | // Names that defines the that parts of runtime information should 45 | // be passed. 46 | FilePos = 1 47 | Function = 2 48 | 49 | stackJump = 2 50 | ) 51 | 52 | // What adds runtime information to the logger context. Remember that 53 | // it returns a slice of pairs so add it this way: 54 | // 55 | // log.Add(where.What(where.Filename, where.Func, where.Line)...) 56 | func What(parts int) []*kiwi.Pair { 57 | var ( 58 | pairs []*kiwi.Pair 59 | skip = stackJump 60 | ) 61 | if parts&FilePos > 0 { 62 | pairs = []*kiwi.Pair{{ 63 | Key: "file", 64 | Eval: func() string { 65 | start: 66 | pc, file, line, _ := runtime.Caller(skip) 67 | function := runtime.FuncForPC(pc).Name() 68 | if strings.LastIndex(function, "grafov/kiwi.") != -1 { 69 | skip++ 70 | goto start 71 | } 72 | return file + ":" + strconv.Itoa(line) 73 | }, 74 | Type: kiwi.StringVal}} 75 | } 76 | if parts&Function > 0 { 77 | pairs = append(pairs, &kiwi.Pair{ 78 | Key: "func", 79 | Eval: func() string { 80 | pc, _, _, _ := runtime.Caller(skip) 81 | return runtime.FuncForPC(pc).Name() 82 | }, 83 | Type: kiwi.StringVal, 84 | }) 85 | } 86 | return pairs 87 | } 88 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, Alexander I.Grafov 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // * Redistributions of source code must retain the above copyright notice, this 8 | // list of conditions and the following disclaimer. 9 | // 10 | // * Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // * Neither the name of kvlog nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | // 29 | // ॐ तारे तुत्तारे तुरे स्व 30 | 31 | /* 32 | Package kiwi is a library with an odd logic that log your application' data in its own strange way. 33 | 34 | WIP. API and features is subject of changes. Use it carefully! 35 | 36 | Features briefly: 37 | 38 | * simple format with explicit key for each log message (*logfmt* like) - for high readability by humans 39 | 40 | * JSON format that so liked by machines 41 | 42 | * ability to define own custom format of the output 43 | 44 | * there are not nailed levels, not hardcoded fields in the format 45 | 46 | * output dynamic filtering (change log verbosity on the fly) 47 | 48 | * can keep context of application 49 | 50 | * it fast enough and careful about memory allocs 51 | 52 | Key feature of `kiwi` logger is dynamic filtering of incoming records. 53 | Instead of checking severety level for decide about pass or not the record to the output, 54 | `kiwi` passes all records to *all* the outputs (they called *sinks* in `kiwi` terminology). 55 | But before actual writing each record checked with a set of filters. 56 | Each sink has its own filter set. 57 | It takes into account record keys, values, ranges of values. 58 | So each sink decides pass the record to a writer or filter it out. 59 | Also any pairs in the record may be hidden: so different sinks may display different parts of the same record. 60 | Other effect is: any record may be written to any number of outputs. 61 | */ 62 | package kiwi 63 | -------------------------------------------------------------------------------- /where/where_test.go: -------------------------------------------------------------------------------- 1 | package where 2 | 3 | /* 4 | Copyright (c) 2016-2019, Alexander I.Grafov 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of kvlog nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ॐ तारे तुत्तारे तुरे स्व 33 | 34 | All tests consists of three parts: 35 | 36 | - arrange structures and initialize objects for use in tests 37 | - act on testing object 38 | - check and assert on results 39 | 40 | These parts separated by empty lines in each test function. 41 | */ 42 | 43 | import ( 44 | "bytes" 45 | "strings" 46 | "testing" 47 | 48 | "github.com/grafov/kiwi" 49 | ) 50 | 51 | func TestWhere_GetAllInfo_Logfmt(t *testing.T) { 52 | stream := bytes.NewBufferString("") 53 | log := kiwi.New() 54 | out := kiwi.SinkTo(stream, kiwi.AsLogfmt()).Start() 55 | 56 | log.With(What(FilePos | Function)) 57 | log.Log("key", "value") 58 | 59 | out.Close() 60 | expected := `file="` 61 | if !strings.Contains(stream.String(), expected) { 62 | t.Logf("expected %s got %v", expected, stream.String()) 63 | t.Fail() 64 | } 65 | expected = `func="` 66 | if !strings.Contains(stream.String(), expected) { 67 | t.Logf("expected %s got %v", expected, stream.String()) 68 | t.Fail() 69 | } 70 | } 71 | 72 | // Test of log to the stopped sink. 73 | func TestWhereGlobal_GetAllInfo_Logfmt(t *testing.T) { 74 | stream := bytes.NewBufferString("") 75 | out := kiwi.SinkTo(stream, kiwi.AsLogfmt()).Start() 76 | 77 | kiwi.With(What(FilePos | Function)) 78 | kiwi.Log("k", "v", "k2", "v2") 79 | 80 | out.Close() 81 | expected := `file="` 82 | if !strings.Contains(stream.String(), expected) { 83 | t.Logf("expected '%s' in '%v'", expected, stream.String()) 84 | t.Fail() 85 | } 86 | expected = `func="` 87 | if !strings.Contains(stream.String(), expected) { 88 | t.Logf("expected '%s' in '%v'", expected, stream.String()) 89 | t.Fail() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /global-logger.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | import "fmt" 4 | 5 | // This file consists of definition of global logging methods. 6 | 7 | /* Copyright (c) 2016-2019, Alexander I.Grafov 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | * Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | * Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | 20 | * Neither the name of kvlog nor the names of its 21 | contributors may be used to endorse or promote products derived from 22 | this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | ॐ तारे तुत्तारे तुरे स्व */ 36 | 37 | // Log is simplified realization of Logger.Log(). 38 | // You would like use it in short applications where context and 39 | // initialization of logger could brought extra complexity. 40 | // If you wish separate contexts and achieve better performance 41 | // use Logger type instead. 42 | func Log(kv ...interface{}) { 43 | // 1. Log the context. 44 | var record = make([]*Pair, 0, len(context)+len(kv)) 45 | global.RLock() 46 | for _, p := range context { 47 | // Evaluate delayed context value here before the output. 48 | if p.Eval != nil { 49 | record = append(record, &Pair{p.Key, p.Eval.(func() string)(), p.Eval, p.Type}) 50 | } else { 51 | record = append(record, &Pair{p.Key, p.Val, nil, p.Type}) 52 | } 53 | } 54 | global.RUnlock() 55 | // 2. Log the regular key-value pairs that came in the args. 56 | var ( 57 | key string 58 | shouldBeAKey = true 59 | ) 60 | for _, val := range kv { 61 | var p *Pair 62 | if shouldBeAKey { 63 | switch v := val.(type) { 64 | case string: 65 | key = v 66 | case *Pair: 67 | if v.Eval != nil { 68 | v.Val = v.Eval.(func() string)() 69 | } 70 | record = append(record, v) 71 | continue 72 | default: 73 | record = append(record, toPair(ErrorKey, fmt.Sprintf("non a string type (%T) for the key (%v)", val, val))) 74 | key = MessageKey 75 | } 76 | } else { 77 | if p = toPair(key, val); p.Eval != nil { 78 | p.Val = p.Eval.(func() string)() 79 | } 80 | record = append(record, p) 81 | } 82 | shouldBeAKey = !shouldBeAKey 83 | } 84 | // Add the value without the key for odd number for key-val pairs. 85 | if !shouldBeAKey && key != MessageKey { 86 | record = append(record, toPair(MessageKey, key)) 87 | } 88 | // 2. Pass the record to the collector. 89 | sinkRecord(record) 90 | } 91 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | // This file consists of implementations of Filter interface. 4 | 5 | /* Copyright (c) 2016-2019, Alexander I.Grafov 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of kvlog nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ॐ तारे तुत्तारे तुरे स्व */ 34 | 35 | import ( 36 | "strconv" 37 | "time" 38 | ) 39 | 40 | // Filter accepts key and value. If the filter passed it should return true. 41 | // Custom filters must conform the interface. 42 | type Filter interface { 43 | Check(string, string) bool 44 | } 45 | 46 | type keyFilter struct { 47 | } 48 | 49 | // Check for the key filter needs only for conforming with Filter interface. 50 | // It always returns true because check for the key already made in sink. 51 | func (*keyFilter) Check(key, val string) bool { 52 | return true 53 | } 54 | 55 | type valsFilter struct { 56 | Vals []string 57 | } 58 | 59 | func (f *valsFilter) Check(key, val string) bool { 60 | for _, v := range f.Vals { 61 | if v == val { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | 68 | type int64RangeFilter struct { 69 | From, To int64 70 | } 71 | 72 | func (f *int64RangeFilter) Check(key, val string) bool { 73 | var ( 74 | intVal int64 75 | err error 76 | ) 77 | if intVal, err = strconv.ParseInt(val, 10, 64); err != nil { 78 | return false 79 | } 80 | return intVal > f.From && intVal <= f.To 81 | } 82 | 83 | type float64RangeFilter struct { 84 | From, To float64 85 | } 86 | 87 | func (f *float64RangeFilter) Check(key, val string) bool { 88 | var ( 89 | floatVal float64 90 | err error 91 | ) 92 | if floatVal, err = strconv.ParseFloat(val, 64); err != nil { 93 | return false 94 | } 95 | return floatVal > f.From && floatVal <= f.To 96 | } 97 | 98 | type timeRangeFilter struct { 99 | From, To time.Time 100 | } 101 | 102 | func (f *timeRangeFilter) Check(key, val string) bool { 103 | var ( 104 | valTime time.Time 105 | err error 106 | ) 107 | if valTime, err = time.Parse(TimeLayout, val); err != nil { 108 | return false 109 | } 110 | if f.From.Before(valTime) && f.To.After(valTime) { 111 | return true 112 | } 113 | return false 114 | } 115 | -------------------------------------------------------------------------------- /loggers_benchmark_json_test.go: -------------------------------------------------------------------------------- 1 | package kiwi_test 2 | 3 | // It was adapted from logxi package tests. 4 | 5 | import ( 6 | "bytes" 7 | "testing" 8 | "time" 9 | 10 | "github.com/grafov/kiwi" 11 | "github.com/grafov/kiwi/level" 12 | "github.com/grafov/kiwi/strict" 13 | "github.com/grafov/kiwi/timestamp" 14 | ) 15 | 16 | // These tests write out all log levels with concurrency turned on and 17 | // (mostly) equivalent fields. 18 | 19 | func BenchmarkLevelsKiwiStrict_JSON(b *testing.B) { 20 | buf := &bytes.Buffer{} 21 | b.ResetTimer() 22 | l := level.New() 23 | l.With("_n", "bench", "_p", pid) 24 | l.With(timestamp.Set(time.RFC3339)) 25 | level.LevelName = "l" 26 | out := kiwi.SinkTo(buf, kiwi.AsJSON()).Start() 27 | for i := 0; i < b.N; i++ { 28 | l.Debug(strict.Int("key", 1), strict.Float64("key2", 3.141592), strict.String("key3", "string"), strict.Bool("key4", false)) 29 | l.Info(strict.Int("key", 1), strict.Float64("key2", 3.141592), strict.String("key3", "string"), strict.Bool("key4", false)) 30 | l.Warn(strict.Int("key", 1), strict.Float64("key2", 3.141592), strict.String("key3", "string"), strict.Bool("key4", false)) 31 | l.Error(strict.Int("key", 1), strict.Float64("key2", 3.141592), strict.String("key3", "string"), strict.Bool("key4", false)) 32 | } 33 | b.StopTimer() 34 | out.Close() 35 | } 36 | 37 | func BenchmarkLevelsKiwiStrictComplex_JSON(b *testing.B) { 38 | buf := &bytes.Buffer{} 39 | b.ResetTimer() 40 | l := level.New() 41 | l.With("_n", "bench", "_p", pid) 42 | l.With(timestamp.Set(time.RFC3339)) 43 | level.LevelName = "l" 44 | out := kiwi.SinkTo(buf, kiwi.AsJSON()).Start() 45 | for i := 0; i < b.N; i++ { 46 | l.Debug(strict.Int("key", 1), strict.Stringer("obj", testObject)) 47 | l.Info(strict.Int("key", 1), strict.Stringer("obj", testObject)) 48 | l.Warn(strict.Int("key", 1), strict.Stringer("obj", testObject)) 49 | l.Error(strict.Int("key", 1), strict.Stringer("obj", testObject)) 50 | } 51 | b.StopTimer() 52 | out.Close() 53 | } 54 | 55 | func BenchmarkLevelsKiwi_JSON(b *testing.B) { 56 | buf := &bytes.Buffer{} 57 | b.ResetTimer() 58 | l := level.New() 59 | l.With("_n", "bench", "_p", pid) 60 | l.With(timestamp.Set(time.RFC3339)) 61 | level.LevelName = "l" 62 | out := kiwi.SinkTo(buf, kiwi.AsJSON()).Start() 63 | for i := 0; i < b.N; i++ { 64 | l.Debug("key", 1, "key2", 3.141592, "key3", "string", "key4", false) 65 | l.Info("key", 1, "key2", 3.141592, "key3", "string", "key4", false) 66 | l.Warn("key", 1, "key2", 3.141592, "key3", "string", "key4", false) 67 | l.Error("key", 1, "key2", 3.141592, "key3", "string", "key4", false) 68 | } 69 | b.StopTimer() 70 | out.Close() 71 | } 72 | 73 | func BenchmarkLevelsKiwiComplex_JSON(b *testing.B) { 74 | buf := &bytes.Buffer{} 75 | b.ResetTimer() 76 | l := level.New() 77 | l.With("_n", "bench", "_p", pid) 78 | l.With(timestamp.Set(time.RFC3339)) 79 | level.LevelName = "l" 80 | out := kiwi.SinkTo(buf, kiwi.AsJSON()).Start() 81 | for i := 0; i < b.N; i++ { 82 | l.Debug("key", 1, "obj", testObject) 83 | l.Info("key", 1, "obj", testObject) 84 | l.Warn("key", 1, "obj", testObject) 85 | l.Error("key", 1, "obj", testObject) 86 | } 87 | b.StopTimer() 88 | out.Close() 89 | } 90 | 91 | func BenchmarkLevelsKiwiGlobal_JSON(b *testing.B) { 92 | buf := &bytes.Buffer{} 93 | b.ResetTimer() 94 | out := kiwi.SinkTo(buf, kiwi.AsJSON()).Start() 95 | for i := 0; i < b.N; i++ { 96 | kiwi.Log("t", time.Now().Format(time.RFC3339), "l", "debug", "_n", "bench", "_p", pid, "key", 1, "key2", 3.141592, "key3", "string", "key4", false) 97 | kiwi.Log("t", time.Now().Format(time.RFC3339), "l", "info", "_n", "bench", "_p", pid, "key", 1, "key2", 3.141592, "key3", "string", "key4", false) 98 | kiwi.Log("t", time.Now().Format(time.RFC3339), "l", "warn", "_n", "bench", "_p", pid, "key", 1, "key2", 3.141592, "key3", "string", "key4", false) 99 | kiwi.Log("t", time.Now().Format(time.RFC3339), "l", "error", "_n", "bench", "_p", pid, "key", 1, "key2", 3.141592, "key3", "string", "key4", false) 100 | } 101 | b.StopTimer() 102 | out.Close() 103 | } 104 | -------------------------------------------------------------------------------- /strict/strict-pairs.go: -------------------------------------------------------------------------------- 1 | package strict 2 | 3 | // This file consists of helpers for adding pairs with strongly typed values. 4 | // I not sure about this part of API yet. It should be moved into a separate 5 | // package or removed. 6 | 7 | /* Copyright (c) 2016-2017, Alexander I.Grafov 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | * Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | * Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | 20 | * Neither the name of kvlog nor the names of its 21 | contributors may be used to endorse or promote products derived from 22 | this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | ॐ तारे तुत्तारे तुरे स्व */ 36 | 37 | import ( 38 | "strconv" 39 | "time" 40 | 41 | "github.com/grafov/kiwi" 42 | ) 43 | 44 | // String formats pair for string. 45 | // Note: type helpers are experimental part of API and may be removed. 46 | func String(key string, val string) *kiwi.Pair { 47 | return &kiwi.Pair{key, val, nil, kiwi.StringVal} 48 | } 49 | 50 | // Stringer formats pair for string. 51 | // Note: type helpers are experimental part of API and may be removed. 52 | func Stringer(key string, val kiwi.Stringer) *kiwi.Pair { 53 | return &kiwi.Pair{key, val.String(), nil, kiwi.StringVal} 54 | } 55 | 56 | // Int formats pair for int value. If you need add integer of specific size just 57 | // convert it to int, int64 or uint64 and use AddInt(), AddInt64() or AddUint64() 58 | // respectively. 59 | // Note: type helpers are experimental part of API and may be removed. 60 | func Int(key string, val int) *kiwi.Pair { 61 | return &kiwi.Pair{key, strconv.Itoa(val), nil, kiwi.IntegerVal} 62 | } 63 | 64 | // Int64 formats pair for int64 value. 65 | // Note: type helpers are experimental part of API and may be removed. 66 | func Int64(key string, val int64) *kiwi.Pair { 67 | return &kiwi.Pair{key, strconv.FormatInt(val, 10), nil, kiwi.IntegerVal} 68 | } 69 | 70 | // Uint64 formats pair for uint64 value. 71 | // Note: type helpers are experimental part of API and may be removed. 72 | func Uint64(key string, val uint64) *kiwi.Pair { 73 | return &kiwi.Pair{key, strconv.FormatUint(val, 10), nil, kiwi.IntegerVal} 74 | } 75 | 76 | // Float64 formats pair for float64 value. If you need add float of other size just 77 | // convert it to float64. 78 | // Note: type helpers are experimental part of API and may be removed. 79 | func Float64(key string, val float64) *kiwi.Pair { 80 | return &kiwi.Pair{key, strconv.FormatFloat(val, 'e', -1, 64), nil, kiwi.FloatVal} 81 | } 82 | 83 | // Bool formats pair for bool value. 84 | // Note: type helpers are experimental part of API and may be removed. 85 | func Bool(key string, val bool) *kiwi.Pair { 86 | if val { 87 | return &kiwi.Pair{key, "true", nil, kiwi.BooleanVal} 88 | } 89 | return &kiwi.Pair{key, "false", nil, kiwi.BooleanVal} 90 | } 91 | 92 | // Time formats pair for time.Time value. 93 | // Note: type helpers are experimental part of API and may be removed. 94 | func Time(key string, val time.Time, layout string) *kiwi.Pair { 95 | return &kiwi.Pair{key, val.Format(layout), nil, kiwi.TimeVal} 96 | } 97 | -------------------------------------------------------------------------------- /global-context.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | // Global context for all logger instances including global logger. 4 | 5 | /* Copyright (c) 2016-2020, Alexander I.Grafov 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of kvlog nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ॐ तारे तुत्तारे तुरे स्व */ 34 | 35 | import ( 36 | "sync" 37 | ) 38 | 39 | var ( 40 | global sync.RWMutex 41 | context []*Pair 42 | ) 43 | 44 | // With adds key-vals to the global logger context. It is safe for 45 | // concurrency. 46 | func With(kv ...interface{}) { 47 | var ( 48 | key string 49 | thisIsKey = true 50 | ) 51 | global.Lock() 52 | next: 53 | for _, arg := range kv { 54 | if thisIsKey { 55 | switch p := arg.(type) { 56 | // The odd arg treated as the keys. The key must be a 57 | // string. 58 | case string: 59 | key = p 60 | // Instead of the key the key-value pair could be 61 | // passed. Next arg should be a key. 62 | case *Pair: 63 | for i, c := range context { 64 | if c.Key == p.Key { 65 | context[i] = p 66 | break next 67 | } 68 | } 69 | context = append(context, p) 70 | continue 71 | // Also the slice of key-value pairs could be passed. Next 72 | // arg should be a key. 73 | case []*Pair: 74 | for _, v := range p { 75 | for i, c := range context { 76 | if c.Key == v.Key { 77 | context[i] = v 78 | break 79 | } 80 | } 81 | context = append(context, v) 82 | } 83 | continue 84 | // The key must be be a string type. The logger generates 85 | // error as a new key-value pair for the record. 86 | default: 87 | context = append(context, toPair(ErrorKey, "wrong type for the key")) 88 | key = MessageKey 89 | } 90 | } else { 91 | p := toPair(key, arg) 92 | for i, c := range context { 93 | if c.Key == key { 94 | context[i] = p 95 | thisIsKey = !thisIsKey 96 | break next 97 | } 98 | } 99 | context = append(context, p) 100 | } 101 | thisIsKey = !thisIsKey 102 | } 103 | if !thisIsKey && key != MessageKey { 104 | context = append(context, toPair(MessageKey, key)) 105 | } 106 | global.Unlock() 107 | } 108 | 109 | // Without drops the keys from the context of the global logger. It is safe for 110 | // concurrency. 111 | func Without(keys ...string) { 112 | global.Lock() 113 | for _, key := range keys { 114 | for i, p := range context { 115 | if p.Key == key { 116 | copy(context[i:], context[i+1:]) 117 | context[len(context)-1] = nil 118 | context = context[:len(context)-1] 119 | break 120 | } 121 | } 122 | } 123 | global.Unlock() 124 | } 125 | 126 | // ResetContext resets the global context for the global logger and 127 | // its descendants. It is safe for concurrency. 128 | func ResetContext() { 129 | global.Lock() 130 | context = nil 131 | global.Unlock() 132 | } 133 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | // The logger instance context. 4 | 5 | /* Copyright (c) 2016-2019, Alexander I.Grafov 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of kvlog nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ॐ तारे तुत्तारे तुरे स्व */ 34 | 35 | import "fmt" 36 | 37 | // With defines a context for the logger. The context overrides pairs 38 | // in the record. The function is not concurrent safe. 39 | func (l *Logger) With(keyVals ...interface{}) *Logger { 40 | var ( 41 | key string 42 | thisIsKey = true 43 | ) 44 | next: 45 | for _, arg := range keyVals { 46 | if thisIsKey { 47 | switch t := arg.(type) { 48 | // The odd arg treated as the keys. The key must be a 49 | // string. 50 | case string: 51 | key = t 52 | // Instead of the key the key-value pair could be 53 | // passed. Next arg should be a key. 54 | case *Pair: 55 | p := t 56 | for i, c := range l.context { 57 | if p.Key == c.Key { 58 | l.context[i] = p 59 | break next 60 | } 61 | } 62 | l.context = append(l.context, p) 63 | continue 64 | // Also the slice of key-value pairs could be passed. Next 65 | // arg should be a key. 66 | case []*Pair: 67 | for _, p := range t { 68 | for i, c := range l.context { 69 | if c.Key == p.Key { 70 | l.context[i] = p 71 | break 72 | } 73 | } 74 | l.context = append(l.context, p) 75 | } 76 | continue 77 | // The key must be be a string type. The logger generates 78 | // error as a new key-value pair for the record. 79 | default: 80 | l.context = append(l.context, toPair(ErrorKey, fmt.Sprintf("non a string type (%T) for the key (%v)", arg, arg))) 81 | key = MessageKey 82 | } 83 | } else { 84 | p := toPair(key, arg) 85 | for i, c := range l.context { 86 | if c.Key == key { 87 | l.context[i] = p 88 | thisIsKey = !thisIsKey 89 | break next 90 | } 91 | } 92 | l.context = append(l.context, toPair(key, arg)) 93 | } 94 | // After the key the next arg is not a key. 95 | thisIsKey = !thisIsKey 96 | } 97 | if !thisIsKey && key != MessageKey { 98 | l.context = append(l.context, toPair(MessageKey, key)) 99 | } 100 | return l 101 | } 102 | 103 | // Without drops some keys from a context for the logger. The function 104 | // is not concurrent safe. 105 | func (l *Logger) Without(keys ...string) *Logger { 106 | for _, k := range keys { 107 | for i, v := range l.context { 108 | if v.Key == k { 109 | copy(l.context[i:], l.context[i+1:]) 110 | l.context[len(l.context)-1] = nil 111 | l.context = l.context[:len(l.context)-1] 112 | break 113 | } 114 | } 115 | } 116 | return l 117 | } 118 | 119 | // ResetContext resets the context of the logger. The function is not 120 | // concurrent safe. 121 | func (l *Logger) ResetContext() *Logger { 122 | l.context = nil 123 | return l 124 | } 125 | -------------------------------------------------------------------------------- /formatter.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | // This file consists of realizations of default formatters. 4 | 5 | /* Copyright (c) 2016-2018, Alexander I.Grafov 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of kvlog nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ॐ तारे तुत्तारे तुरे स्व */ 34 | 35 | import ( 36 | "bytes" 37 | "strconv" 38 | "strings" 39 | ) 40 | 41 | // Formatter represents format of the output. 42 | type Formatter interface { 43 | // Begin function allows to add prefix string for the output 44 | // or make some preparations before the output. 45 | Begin() 46 | // Pair function called for each key-value pair of the record. 47 | // ValueType is hint that helps the formatter decide how to output the value. 48 | // For example formatter can format string values in quotes but numbers without them. 49 | Pair(key, value string, valueType int) 50 | // Finish function allows to add suffix string for the output. 51 | // Also it returns result string for the displaying of the single record. 52 | // It may be multiline if you wish. Result has no restrictions for you imagination :) 53 | Finish() []byte 54 | } 55 | 56 | type formatLogfmt struct { 57 | line *bytes.Buffer 58 | } 59 | 60 | // AsLogfmt says that a sink uses Logfmt format for records output. 61 | func AsLogfmt() *formatLogfmt { 62 | return &formatLogfmt{line: bytes.NewBuffer(make([]byte, 256))} 63 | } 64 | 65 | func (f *formatLogfmt) Begin() { 66 | f.line.Reset() 67 | } 68 | 69 | func (f *formatLogfmt) Pair(key, val string, valType int) { 70 | // TODO allow multiline values output? 71 | // TODO extend check for all non printable chars, so it need just check for each byte>space 72 | if strings.ContainsAny(key, " \n\r\t") { 73 | f.line.WriteString(strconv.Quote(key)) 74 | } else { 75 | f.line.WriteString(key) 76 | } 77 | switch valType { 78 | case StringVal, CustomQuoted: 79 | f.line.WriteRune('=') 80 | f.line.WriteString(strconv.Quote(val)) 81 | default: 82 | f.line.WriteRune('=') 83 | f.line.WriteString(val) 84 | } 85 | f.line.WriteRune(' ') 86 | } 87 | 88 | func (f *formatLogfmt) Finish() []byte { 89 | f.line.WriteRune('\n') 90 | return f.line.Bytes() 91 | } 92 | 93 | type formatJSON struct { 94 | line *bytes.Buffer 95 | } 96 | 97 | // AsJSON says that a sink uses JSON (RFC-7159) format for records output. 98 | func AsJSON() *formatJSON { 99 | return &formatJSON{line: bytes.NewBuffer(make([]byte, 256))} 100 | } 101 | 102 | func (f *formatJSON) Begin() { 103 | f.line.Reset() 104 | f.line.WriteRune('{') 105 | } 106 | 107 | func (f *formatJSON) Pair(key, val string, valType int) { 108 | f.line.WriteString(strconv.Quote(key)) 109 | f.line.WriteRune(':') 110 | switch valType { 111 | case StringVal, TimeVal, CustomQuoted: 112 | f.line.WriteString(strconv.Quote(val)) 113 | default: 114 | f.line.WriteString(val) 115 | } 116 | f.line.WriteString(", ") 117 | } 118 | 119 | func (f *formatJSON) Finish() []byte { 120 | f.line.WriteRune('}') 121 | f.line.WriteRune('\n') 122 | return f.line.Bytes() 123 | } 124 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | /* 4 | Copyright (c) 2016-2019, Alexander I.Grafov 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of kvlog nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ॐ तारे तुत्तारे तुरे स्व 33 | */ 34 | 35 | import ( 36 | "testing" 37 | ) 38 | 39 | /* All tests consists of three parts: 40 | 41 | - arrange structures and initialize objects for use in tests 42 | - act on testing object 43 | - check and assert on results 44 | 45 | These parts separated by empty lines in each test function. 46 | */ 47 | 48 | // Get context from logger. Helper for testing. 49 | func (l *Logger) getAllContext() []*Pair { 50 | return l.context 51 | } 52 | 53 | func (l *Logger) checkContext(key string) string { 54 | for _, v := range l.context { 55 | if v.Key == key { 56 | return v.Val 57 | } 58 | } 59 | return "" 60 | } 61 | 62 | func TestNewLogger(t *testing.T) { 63 | l := New() 64 | 65 | if l == nil { 66 | t.Fatal("initialized logger is nil") 67 | } 68 | } 69 | 70 | // Test of creating a fork the logger from the existing logger. 71 | func TestLogger_ForkWithContext(t *testing.T) { 72 | log := Fork().With("key", "value", "key2", 123) 73 | 74 | sub := log.Fork() 75 | 76 | if sub.checkContext("key") != "value" { 77 | t.Logf("expected %s got %v", "value", log.checkContext("key")) 78 | t.Fail() 79 | } 80 | if sub.checkContext("key2") != "123" { 81 | t.Logf("expected %s got %v", "123", log.checkContext("key2")) 82 | t.Fail() 83 | } 84 | } 85 | 86 | // Test of a new sublogger without context inheritance (because it 87 | // should use Fork() for copy the context). 88 | func TestLogger_NewWithContext(t *testing.T) { 89 | log := New().With("key", "value", "key2", 123) 90 | 91 | sub := log.New() 92 | 93 | context := sub.getAllContext() 94 | if len(context) != 0 { 95 | t.Logf("expected empty context but got %v", context) 96 | t.FailNow() 97 | } 98 | } 99 | 100 | // Test of a new sublogger without context inheritance (because it 101 | // should use Fork() for copy the context). 102 | func TestLogger_NewWithPartiallySameContext(t *testing.T) { 103 | log := New().With("k", "v", "k2", "v2") 104 | 105 | sub := log.New().With("k2", "v2") 106 | 107 | if sub.checkContext("k") != "" { 108 | t.Logf(`expected empty context but got %v`, context) 109 | t.FailNow() 110 | } 111 | if sub.checkContext("k2") != "v2" { 112 | t.Logf(`expected empty v2 but got %v`, context) 113 | t.FailNow() 114 | } 115 | } 116 | 117 | // Test of creating a new logger from existing logger. Deleted values should not present in sublogger. 118 | func TestLogger_ForkWithPartialContext(t *testing.T) { 119 | log := Fork().With("key", "value", "key2", "value2") 120 | 121 | log.Without("key") 122 | sub := log.Fork() 123 | 124 | if sub.getAllContext() == nil { 125 | t.Logf("context is nil but should not") 126 | t.FailNow() 127 | } 128 | if sub.checkContext("key") != "" { 129 | t.Logf(`expected nothing got %v`, sub.checkContext("key")) 130 | t.Fail() 131 | 132 | } 133 | if sub.checkContext("key2") == "" || sub.checkContext("key2") != "value2" { 134 | t.Logf(`expected "value2" got %v`, sub.checkContext("key2")) 135 | t.Fail() 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /loggers_benchmark_logfmt_test.go: -------------------------------------------------------------------------------- 1 | package kiwi_test 2 | 3 | // It was adapted from logxi package tests. 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/grafov/kiwi" 13 | "github.com/grafov/kiwi/level" 14 | "github.com/grafov/kiwi/strict" 15 | "github.com/grafov/kiwi/timestamp" 16 | ) 17 | 18 | type M map[string]interface{} 19 | 20 | var testObject = M{ 21 | "foo": "bar", 22 | "bah": M{ 23 | "int": 1, 24 | "float": -100.23, 25 | "date": "06-01-01T15:04:05-0700", 26 | "bool": true, 27 | "nullable": nil, 28 | }, 29 | } 30 | 31 | // Right way for kiwi is realize Record interface for the custom type 32 | // that logger can't accept directly. But you can simply pass fmt.Stringer 33 | // interface as well. 34 | // You need Record interface if you want specify quotation rules with IsQuoted(). 35 | // Elsewere String() is enough. 36 | func (m M) String() string { 37 | b, _ := json.Marshal(m) 38 | return string(b) 39 | } 40 | 41 | var pid = os.Getpid() 42 | 43 | func toJSON(m map[string]interface{}) string { 44 | b, _ := json.Marshal(m) 45 | return string(b) 46 | } 47 | 48 | // These tests write out all log levels with concurrency turned on and 49 | // (mostly) equivalent fields. 50 | 51 | func BenchmarkLevelsKiwiStrict_Logfmt(b *testing.B) { 52 | buf := &bytes.Buffer{} 53 | b.ResetTimer() 54 | l := level.New() 55 | l.With("_n", "bench", "_p", pid) 56 | l.With(timestamp.Set(time.RFC3339)) 57 | level.LevelName = "l" 58 | out := kiwi.SinkTo(buf, kiwi.AsLogfmt()).Start() 59 | for i := 0; i < b.N; i++ { 60 | l.Debug(strict.Int("key", 1), strict.Float64("key2", 3.141592), strict.String("key3", "string"), strict.Bool("key4", false)) 61 | l.Info(strict.Int("key", 1), strict.Float64("key2", 3.141592), strict.String("key3", "string"), strict.Bool("key4", false)) 62 | l.Warn(strict.Int("key", 1), strict.Float64("key2", 3.141592), strict.String("key3", "string"), strict.Bool("key4", false)) 63 | l.Error(strict.Int("key", 1), strict.Float64("key2", 3.141592), strict.String("key3", "string"), strict.Bool("key4", false)) 64 | } 65 | b.StopTimer() 66 | out.Close() 67 | } 68 | 69 | func BenchmarkLevelsKiwiStrictComplex_Logfmt(b *testing.B) { 70 | buf := &bytes.Buffer{} 71 | b.ResetTimer() 72 | l := level.New() 73 | l.With("_n", "bench", "_p", pid) 74 | l.With(timestamp.Set(time.RFC3339)) 75 | level.LevelName = "l" 76 | out := kiwi.SinkTo(buf, kiwi.AsLogfmt()).Start() 77 | for i := 0; i < b.N; i++ { 78 | l.Debug(strict.Int("key", 1), strict.Stringer("obj", testObject)) 79 | l.Info(strict.Int("key", 1), strict.Stringer("obj", testObject)) 80 | l.Warn(strict.Int("key", 1), strict.Stringer("obj", testObject)) 81 | l.Error(strict.Int("key", 1), strict.Stringer("obj", testObject)) 82 | } 83 | b.StopTimer() 84 | out.Close() 85 | } 86 | 87 | func BenchmarkLevelsKiwi_Logfmt(b *testing.B) { 88 | buf := &bytes.Buffer{} 89 | b.ResetTimer() 90 | l := level.New() 91 | l.With("_n", "bench", "_p", pid) 92 | l.With(timestamp.Set(time.RFC3339)) 93 | level.LevelName = "l" 94 | out := kiwi.SinkTo(buf, kiwi.AsLogfmt()).Start() 95 | for i := 0; i < b.N; i++ { 96 | l.Debug("key", 1, "key2", 3.141592, "key3", "string", "key4", false) 97 | l.Info("key", 1, "key2", 3.141592, "key3", "string", "key4", false) 98 | l.Warn("key", 1, "key2", 3.141592, "key3", "string", "key4", false) 99 | l.Error("key", 1, "key2", 3.141592, "key3", "string", "key4", false) 100 | } 101 | b.StopTimer() 102 | out.Close() 103 | } 104 | 105 | func BenchmarkLevelsKiwiComplex_Logfmt(b *testing.B) { 106 | buf := &bytes.Buffer{} 107 | b.ResetTimer() 108 | l := level.New() 109 | l.With("_n", "bench", "_p", pid) 110 | l.With(timestamp.Set(time.RFC3339)) 111 | level.LevelName = "l" 112 | out := kiwi.SinkTo(buf, kiwi.AsLogfmt()).Start() 113 | for i := 0; i < b.N; i++ { 114 | l.Debug("key", 1, "obj", testObject) 115 | l.Info("key", 1, "obj", testObject) 116 | l.Warn("key", 1, "obj", testObject) 117 | l.Error("key", 1, "obj", testObject) 118 | } 119 | b.StopTimer() 120 | out.Close() 121 | } 122 | 123 | func BenchmarkLevelsKiwiGlobal_Logfmt(b *testing.B) { 124 | buf := &bytes.Buffer{} 125 | b.ResetTimer() 126 | out := kiwi.SinkTo(buf, kiwi.AsLogfmt()).Start() 127 | for i := 0; i < b.N; i++ { 128 | kiwi.Log("t", time.Now().Format(time.RFC3339), "l", "debug", "_n", "bench", "_p", pid, "key", 1, "key2", 3.141592, "key3", "string", "key4", false) 129 | kiwi.Log("t", time.Now().Format(time.RFC3339), "l", "info", "_n", "bench", "_p", pid, "key", 1, "key2", 3.141592, "key3", "string", "key4", false) 130 | kiwi.Log("t", time.Now().Format(time.RFC3339), "l", "warn", "_n", "bench", "_p", pid, "key", 1, "key2", 3.141592, "key3", "string", "key4", false) 131 | kiwi.Log("t", time.Now().Format(time.RFC3339), "l", "error", "_n", "bench", "_p", pid, "key", 1, "key2", 3.141592, "key3", "string", "key4", false) 132 | } 133 | b.StopTimer() 134 | out.Close() 135 | } 136 | -------------------------------------------------------------------------------- /convertor.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | // Convert incoming values to string representation. For keys and values. 4 | 5 | /* Copyright (c) 2016-2020, Alexander I.Grafov 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of kvlog nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ॐ तारे तुत्तारे तुरे स्व */ 34 | 35 | import ( 36 | "encoding" 37 | "fmt" 38 | "strconv" 39 | "time" 40 | ) 41 | 42 | // Possible kinds of logged values. 43 | const ( 44 | // BooleanVal and other types below commonly formatted unquoted. 45 | // But it depends on the formatter. 46 | BooleanVal = iota 47 | IntegerVal 48 | FloatVal 49 | ComplexVal 50 | CustomUnquoted 51 | // VoidVal and other types below commonly formatted unquoted. 52 | // But it depends on the formatter. 53 | StringVal 54 | TimeVal 55 | CustomQuoted 56 | ) 57 | 58 | // FloatFormat used in Float to String conversion. 59 | // It is second parameter passed to strconv.FormatFloat() 60 | var FloatFormat byte = 'e' 61 | 62 | // TimeLayout used in time.Time to String conversion. 63 | var TimeLayout = time.RFC3339 64 | 65 | // it applicable for all scalar types and for strings 66 | func toPair(key string, val interface{}) *Pair { 67 | switch v := val.(type) { 68 | case string: 69 | return &Pair{key, v, nil, StringVal} 70 | case []byte: 71 | return &Pair{key, string(v), nil, StringVal} 72 | case bool: 73 | if val.(bool) { 74 | return &Pair{key, "true", nil, BooleanVal} 75 | } 76 | return &Pair{key, "false", nil, BooleanVal} 77 | case int: 78 | return &Pair{key, strconv.Itoa(v), nil, IntegerVal} 79 | case int8: 80 | return &Pair{key, strconv.FormatInt(int64(v), 10), nil, IntegerVal} 81 | case int16: 82 | return &Pair{key, strconv.FormatInt(int64(v), 10), nil, IntegerVal} 83 | case int32: 84 | return &Pair{key, strconv.FormatInt(int64(v), 10), nil, IntegerVal} 85 | case int64: 86 | return &Pair{key, strconv.FormatInt(v, 10), nil, IntegerVal} 87 | case uint: 88 | return &Pair{key, strconv.FormatUint(uint64(v), 10), nil, IntegerVal} 89 | case uint8: 90 | return &Pair{key, strconv.FormatUint(uint64(v), 10), nil, IntegerVal} 91 | case uint16: 92 | return &Pair{key, strconv.FormatUint(uint64(v), 10), nil, IntegerVal} 93 | case uint32: 94 | return &Pair{key, strconv.FormatUint(uint64(v), 10), nil, IntegerVal} 95 | case uint64: 96 | return &Pair{key, strconv.FormatUint(v, 10), nil, IntegerVal} 97 | case float32: 98 | return &Pair{key, strconv.FormatFloat(float64(v), FloatFormat, -1, 32), nil, FloatVal} 99 | case float64: 100 | return &Pair{key, strconv.FormatFloat(v, FloatFormat, -1, 64), nil, FloatVal} 101 | case complex64: 102 | return &Pair{key, fmt.Sprintf("%f", v), nil, ComplexVal} 103 | case complex128: 104 | return &Pair{key, fmt.Sprintf("%f", v), nil, ComplexVal} 105 | case time.Time: 106 | return &Pair{key, v.Format(TimeLayout), nil, TimeVal} 107 | case Valuer: 108 | var pairType = CustomUnquoted 109 | if v.IsQuoted() { 110 | pairType = CustomQuoted 111 | } 112 | return &Pair{key, v.String(), nil, pairType} 113 | case Stringer: 114 | return &Pair{key, v.String(), nil, StringVal} 115 | case encoding.TextMarshaler: 116 | data, err := v.MarshalText() 117 | if err != nil { 118 | return &Pair{key, fmt.Sprintf("%s", err), nil, StringVal} 119 | } 120 | return &Pair{key, string(data), nil, StringVal} 121 | case func() string: 122 | return &Pair{key, "", v, StringVal} 123 | default: 124 | // Worst case conversion that depends on reflection. 125 | return &Pair{key, fmt.Sprintf("%+v", val), nil, StringVal} 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /level/global-levels.go: -------------------------------------------------------------------------------- 1 | package level 2 | 3 | // This file consists of Logger methods for imitating oldschool logging with levels. 4 | 5 | /* Copyright (c) 2016-2017, Alexander I.Grafov 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of kvlog nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ॐ तारे तुत्तारे तुरे स्व */ 34 | 35 | import "github.com/grafov/kiwi" 36 | 37 | // LevelName allows to change default recVal "level" to any recVal you want. 38 | // Set it to empty string if you want to report level without presetting any name. 39 | var LevelName = "level" 40 | 41 | // Fatal imitates behaviour of common loggers with severity levels. It adds a record 42 | // with severity "level" = "fatal". Default severity name "level" may be changed 43 | // globally for all package with UseLevelName(). There is nothing special in "level" 44 | // key so it may be overrided with any value what you want. 45 | func Fatal(keyVals ...interface{}) { 46 | if len(keyVals) == 1 { 47 | kiwi.Log(LevelName, "fatal", kiwi.MessageKey, keyVals[0]) 48 | } else { 49 | kiwi.Log(append(keyVals, LevelName, "fatal")...) 50 | } 51 | } 52 | 53 | // Crit imitates behaviour of common loggers with severity levels. It adds a record 54 | // with severity "level" = "critical". Default severity name "level" may be changed 55 | // globally for all package with UseLevelName(). There is nothing special in "level" 56 | // key so it may be overrided with any value what you want. 57 | func Crit(keyVals ...interface{}) { 58 | if len(keyVals) == 1 { 59 | kiwi.Log(LevelName, "critical", kiwi.MessageKey, keyVals[0]) 60 | } else { 61 | kiwi.Log(append(keyVals, LevelName, "critical")...) 62 | } 63 | } 64 | 65 | // Error imitates behaviour of common loggers with severity levels. It adds a record 66 | // with severity "level" = "error". Default severity name "level" may be changed 67 | // globally for all package with UseLevelName(). There is nothing special in "level" 68 | // key so it may be overrided with any recVal you want. 69 | func Error(keyVals ...interface{}) { 70 | if len(keyVals) == 1 { 71 | kiwi.Log(LevelName, "error", kiwi.MessageKey, keyVals[0]) 72 | } else { 73 | kiwi.Log(append(keyVals, LevelName, "error")...) 74 | } 75 | } 76 | 77 | // Warn imitates behaviour of common loggers with severity levels. It adds a record 78 | // with severity "level" = "warning". Default severity name "level" may be changed 79 | // globally for all package with UseLevelName(). There is nothing special in "level" 80 | // key so it may be overrided with any recVal you want. 81 | func Warn(keyVals ...interface{}) { 82 | if len(keyVals) == 1 { 83 | kiwi.Log(LevelName, "warning", kiwi.MessageKey, keyVals[0]) 84 | } else { 85 | kiwi.Log(append(keyVals, LevelName, "warning")...) 86 | } 87 | } 88 | 89 | // Info imitates behaviour of common loggers with severity levels. It adds a record 90 | // with severity "level" = "info". Default severity name "level" may be changed 91 | // globally for all package with UseLevelName(). There is nothing special in "level" 92 | // key so it may be overrided with any value what you want. 93 | func Info(keyVals ...interface{}) { 94 | if len(keyVals) == 1 { 95 | kiwi.Log(LevelName, "info", kiwi.MessageKey, keyVals[0]) 96 | } else { 97 | kiwi.Log(append(keyVals, LevelName, "info")...) 98 | } 99 | } 100 | 101 | // Debug imitates behaviour of common loggers with severity levels. It adds a record 102 | // with severity "level" = "debug". Default severity name "level" may be changed 103 | // globally for all package with UseLevelName(). There is nothing special in "level" 104 | // key so it may be overrided with any value what you want. 105 | func Debug(keyVals ...interface{}) { 106 | if len(keyVals) == 1 { 107 | kiwi.Log(LevelName, "debug", kiwi.MessageKey, keyVals[0]) 108 | } else { 109 | kiwi.Log(append(keyVals, LevelName, "debug")...) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /level/levels.go: -------------------------------------------------------------------------------- 1 | package level 2 | 3 | // This file consists of Logger methods for imitating oldschool logging with levels. 4 | 5 | /* Copyright (c) 2016-2017, Alexander I.Grafov 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of kvlog nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | ॐ तारे तुत्तारे तुरे स्व */ 34 | 35 | import "github.com/grafov/kiwi" 36 | 37 | type Logger struct { 38 | *kiwi.Logger 39 | } 40 | 41 | // New creates a new leveled logger instance. 42 | func New() *Logger { 43 | return &Logger{kiwi.New()} 44 | } 45 | 46 | // Fatal imitates behaviour of common loggers with severity levels. It adds a record 47 | // with severity "level" = "fatal". Default severity name "level" may be changed 48 | // globally for all package with UseLevelName(). There is nothing special in "level" 49 | // key so it may be overrided with any value what you want. 50 | // 51 | // By design Fatal level doesn't call os.Exit() like other loggers do. 52 | func (l *Logger) Fatal(keyVals ...interface{}) { 53 | if len(keyVals) == 1 { 54 | l.Log(LevelName, "fatal", kiwi.MessageKey, keyVals[0]) 55 | } else { 56 | l.Log(append(keyVals, LevelName, "fatal")...) 57 | } 58 | } 59 | 60 | // Crit imitates behaviour of common loggers with severity levels. It adds a record 61 | // with severity "level" = "critical". Default severity name "level" may be changed 62 | // globally for all package with UseLevelName(). There is nothing special in "level" 63 | // key so it may be overrided with any value what you want. 64 | func (l *Logger) Crit(keyVals ...interface{}) { 65 | if len(keyVals) == 1 { 66 | l.Log(LevelName, "critical", kiwi.MessageKey, keyVals[0]) 67 | } else { 68 | l.Log(append(keyVals, LevelName, "critical")...) 69 | } 70 | } 71 | 72 | // Error imitates behaviour of common loggers with severity levels. It adds a record 73 | // with severity "level" = "error". Default severity name "level" may be changed 74 | // globally for all package with UseLevelName(). There is nothing special in "level" 75 | // key so it may be overrided with any recVal you want. 76 | func (l *Logger) Error(keyVals ...interface{}) { 77 | if len(keyVals) == 1 { 78 | l.Log(LevelName, "error", kiwi.MessageKey, keyVals[0]) 79 | } else { 80 | l.Log(append(keyVals, LevelName, "error")...) 81 | } 82 | } 83 | 84 | // Warn imitates behaviour of common loggers with severity levels. It adds a record 85 | // with severity "level" = "warning". Default severity name "level" may be changed 86 | // globally for all package with UseLevelName(). There is nothing special in "level" 87 | // key so it may be overrided with any recVal you want. 88 | func (l *Logger) Warn(keyVals ...interface{}) { 89 | if len(keyVals) == 1 { 90 | l.Log(LevelName, "warning", kiwi.MessageKey, keyVals[0]) 91 | } else { 92 | l.Log(append(keyVals, LevelName, "warning")...) 93 | } 94 | } 95 | 96 | // Info imitates behaviour of common loggers with severity levels. It adds a record 97 | // with severity "level" = "info". Default severity name "level" may be changed 98 | // globally for all package with UseLevelName(). There is nothing special in "level" 99 | // key so it may be overrided with any value what you want. 100 | func (l *Logger) Info(keyVals ...interface{}) { 101 | if len(keyVals) == 1 { 102 | l.Log(LevelName, "info", kiwi.MessageKey, keyVals[0]) 103 | } else { 104 | l.Log(append(keyVals, LevelName, "info")...) 105 | } 106 | } 107 | 108 | // Debug imitates behaviour of common loggers with severity levels. It adds a record 109 | // with severity "level" = "debug". Default severity name "level" may be changed 110 | // globally for all package with UseLevelName(). There is nothing special in "level" 111 | // key so it may be overrided with any value what you want. 112 | func (l *Logger) Debug(keyVals ...interface{}) { 113 | if len(keyVals) == 1 { 114 | l.Log(LevelName, "debug", kiwi.MessageKey, keyVals[0]) 115 | } else { 116 | l.Log(append(keyVals, LevelName, "debug")...) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /level/levels_test.go: -------------------------------------------------------------------------------- 1 | package level 2 | 3 | /* Copyright (c) 2016, Alexander I.Grafov 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of kvlog nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | ॐ तारे तुत्तारे तुरे स्व 32 | 33 | All tests consists of three parts: 34 | 35 | - arrange structures and initialize objects for use in tests 36 | - act on testing object 37 | - check and assert on results 38 | 39 | These parts separated by empty lines in each test function. 40 | */ 41 | 42 | import ( 43 | "bytes" 44 | "strings" 45 | "testing" 46 | 47 | "github.com/grafov/kiwi" 48 | ) 49 | 50 | // Test of log with fatal level with empty value. Useless but function allow it. 51 | func TestLoggerLevels_LogFatalEmpty_Logfmt(t *testing.T) { 52 | output := bytes.NewBufferString("") 53 | log := New() 54 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 55 | defer out.Close() 56 | 57 | log.Fatal() 58 | 59 | out.Flush() 60 | if strings.TrimSpace(output.String()) != "level=\"fatal\"" { 61 | println(output.String()) 62 | t.Fail() 63 | } 64 | } 65 | 66 | // Test of log with fatal level without a key. 67 | func TestLoggerLevels_LogFatal_Logfmt(t *testing.T) { 68 | output := bytes.NewBufferString("") 69 | log := New() 70 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 71 | defer out.Close() 72 | 73 | log.Fatal("The sample message.") 74 | 75 | out.Flush() 76 | if strings.TrimSpace(output.String()) != `level="fatal" message="The sample message."` { 77 | println(output.String()) 78 | t.Fail() 79 | } 80 | } 81 | 82 | // Test of log with fatal level with a key. 83 | func TestLoggerLevels_LogFatalWKey_Logfmt(t *testing.T) { 84 | output := bytes.NewBufferString("") 85 | log := New() 86 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 87 | defer out.Close() 88 | 89 | log.Fatal("msg", "The sample message.") 90 | 91 | out.Flush() 92 | if strings.TrimSpace(output.String()) != `msg="The sample message." level="fatal"` { 93 | t.Fail() 94 | } 95 | } 96 | 97 | // Test of log with critical level without a key. 98 | func TestLoggerLevels_LogCrit_Logfmt(t *testing.T) { 99 | output := bytes.NewBufferString("") 100 | log := New() 101 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 102 | defer out.Close() 103 | 104 | log.Crit("The sample message.") 105 | 106 | out.Flush() 107 | if strings.TrimSpace(output.String()) != `level="critical" message="The sample message."` { 108 | println(output.String()) 109 | t.Fail() 110 | } 111 | } 112 | 113 | // Test of log with critical level with a key. 114 | func TestLoggerLevels_LogCritWKey_Logfmt(t *testing.T) { 115 | output := bytes.NewBufferString("") 116 | log := New() 117 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 118 | defer out.Close() 119 | 120 | log.Crit("msg", "The sample message.") 121 | 122 | out.Flush() 123 | if strings.TrimSpace(output.String()) != `msg="The sample message." level="critical"` { 124 | println(output.String()) 125 | t.Fail() 126 | } 127 | } 128 | 129 | // Test of log with error level without a key. 130 | func TestLoggerLevels_LogError_Logfmt(t *testing.T) { 131 | output := bytes.NewBufferString("") 132 | log := New() 133 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 134 | defer out.Close() 135 | 136 | log.Error("The sample message.") 137 | 138 | out.Flush() 139 | if strings.TrimSpace(output.String()) != `level="error" message="The sample message."` { 140 | println(output.String()) 141 | t.Fail() 142 | } 143 | } 144 | 145 | // Test of log with error level with a key. 146 | func TestLoggerLevels_LogErrorWKey_Logfmt(t *testing.T) { 147 | output := bytes.NewBufferString("") 148 | log := New() 149 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 150 | defer out.Close() 151 | 152 | log.Error("msg", "The sample message.") 153 | 154 | out.Flush() 155 | if strings.TrimSpace(output.String()) != `msg="The sample message." level="error"` { 156 | println(output.String()) 157 | t.Fail() 158 | } 159 | } 160 | 161 | // Test of log with warning level without a key. 162 | func TestLoggerLevels_LogWarn_Logfmt(t *testing.T) { 163 | output := bytes.NewBufferString("") 164 | log := New() 165 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 166 | defer out.Close() 167 | 168 | log.Warn("The sample message.") 169 | 170 | out.Flush() 171 | if strings.TrimSpace(output.String()) != `level="warning" message="The sample message."` { 172 | println(output.String()) 173 | t.Fail() 174 | } 175 | } 176 | 177 | // Test of log with warning level with a key. 178 | func TestLoggerLevels_LogWarnWKey_Logfmt(t *testing.T) { 179 | output := bytes.NewBufferString("") 180 | log := New() 181 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 182 | defer out.Close() 183 | 184 | log.Warn("msg", "The sample message.") 185 | 186 | out.Flush() 187 | if strings.TrimSpace(output.String()) != `msg="The sample message." level="warning"` { 188 | println(output.String()) 189 | t.Fail() 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | import "fmt" 4 | 5 | // This file consists of Logger related structures and functions. 6 | 7 | /* Copyright (c) 2016-2019, Alexander I.Grafov 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | * Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | * Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | 20 | * Neither the name of kvlog nor the names of its 21 | contributors may be used to endorse or promote products derived from 22 | this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | ॐ तारे तुत्तारे तुरे स्व */ 36 | 37 | var ( 38 | MessageKey = "message" 39 | ErrorKey = "kiwi-error" 40 | InfoKey = "kiwi-info" 41 | ) 42 | 43 | type ( 44 | // Logger keeps context and log record. There are many loggers initialized 45 | // in different places of application. Loggers are not safe for 46 | // concurrent usage so then you need logger for another goroutine you will need clone existing instance. 47 | // See Logger.New() method below for details. 48 | Logger struct { 49 | context []*Pair 50 | pairs []*Pair 51 | } 52 | // Stringer is the same as fmt.Stringer 53 | Stringer interface { 54 | String() string 55 | } 56 | // Valuer allows log data from any custom types if they conform this interface. 57 | // Also types that conform fmt.Stringer can be used. But as they not have IsQuoted() check 58 | // they always treated as strings and displayed in quotes. 59 | Valuer interface { 60 | Stringer 61 | IsQuoted() bool 62 | } 63 | // Pair is key and value together. They can be used by custom 64 | // helpers for example for logging timestamps or something. 65 | Pair struct { 66 | Key string 67 | Val string 68 | Eval interface{} 69 | Type int 70 | } 71 | ) 72 | 73 | // Fork creates a new logger instance that inherited the context from 74 | // the global logger. Thi fuction is concurrent safe. 75 | func Fork() *Logger { 76 | var newContext = make([]*Pair, len(context)) 77 | global.RLock() 78 | copy(newContext, context) 79 | global.RUnlock() 80 | return &Logger{context: newContext} 81 | } 82 | 83 | // New creates a new logger instance but not copy context from the 84 | // global logger. The method is empty now, I keep it for compatibility 85 | // with older versions of API. 86 | func New() *Logger { 87 | return new(Logger) 88 | } 89 | 90 | // Fork creates a new instance of the logger. It copies the context 91 | // from the logger from the parent logger. But the values of the 92 | // current record of the parent logger discarded. 93 | func (l *Logger) Fork() *Logger { 94 | var fork = Logger{context: make([]*Pair, len(l.context))} 95 | copy(fork.context, l.context) 96 | return &fork 97 | } 98 | 99 | // New creates a new instance of the logger. It not inherited the 100 | // context of the parent logger. The method is empty now, I keep it 101 | // for compatibility with older versions of API. 102 | func (l *Logger) New() *Logger { 103 | return new(Logger) 104 | } 105 | 106 | // Log is the most common method for flushing previously added key-val pairs to an output. 107 | // After current record is flushed all pairs removed from a record except contextSrc pairs. 108 | func (l *Logger) Log(keyVals ...interface{}) { 109 | // 1. Log the context. 110 | var record = make([]*Pair, 0, len(l.context)+len(l.pairs)+len(keyVals)) 111 | for _, p := range l.context { 112 | if p.Eval != nil { 113 | // Evaluate delayed context value here before output. 114 | record = append(record, &Pair{p.Key, p.Eval.(func() string)(), p.Eval, p.Type}) 115 | } else { 116 | record = append(record, &Pair{p.Key, p.Val, nil, p.Type}) 117 | } 118 | } 119 | // 2. Log the regular key-value pairs that added before by Add() calls. 120 | for _, p := range l.pairs { 121 | if p.Eval != nil { 122 | record = append(record, &Pair{p.Key, p.Eval.(func() string)(), p.Eval, p.Type}) 123 | } else { 124 | record = append(record, &Pair{p.Key, p.Val, nil, p.Type}) 125 | } 126 | } 127 | // 3. Log the regular key-value pairs that come in the args. 128 | var ( 129 | key string 130 | shouldBeAKey = true 131 | ) 132 | for _, val := range keyVals { 133 | if shouldBeAKey { 134 | switch v := val.(type) { 135 | case string: 136 | key = v 137 | case *Pair: 138 | record = append(record, v) 139 | continue 140 | default: 141 | record = append(record, toPair(ErrorKey, fmt.Sprintf("non a string type (%T) for the key (%v)", val, val))) 142 | key = MessageKey 143 | } 144 | } else { 145 | record = append(record, toPair(key, val)) 146 | } 147 | shouldBeAKey = !shouldBeAKey 148 | } 149 | if !shouldBeAKey && key != MessageKey { 150 | record = append(record, toPair(MessageKey, key)) 151 | } 152 | // 4. Pass the record to the collector. 153 | sinkRecord(record) 154 | l.pairs = nil 155 | } 156 | 157 | // Add a new key-value pairs to the log record. If a key already added then value will be 158 | // updated. If a key already exists in a contextSrc then it will be overridden by a new 159 | // value for a current record only. After flushing a record with Log() old context value 160 | // will be restored. 161 | func (l *Logger) Add(keyVals ...interface{}) *Logger { 162 | var ( 163 | key string 164 | shouldBeAKey = true 165 | ) 166 | // key=val pairs 167 | for _, val := range keyVals { 168 | if shouldBeAKey { 169 | switch v := val.(type) { 170 | case string: 171 | key = v 172 | case *Pair: 173 | l.pairs = append(l.pairs, v) 174 | continue 175 | default: 176 | l.pairs = append(l.pairs, toPair(ErrorKey, fmt.Sprintf("non a string type (%T) for the key (%v)", val, val))) 177 | continue 178 | } 179 | } else { 180 | l.pairs = append(l.pairs, toPair(key, val)) 181 | } 182 | shouldBeAKey = !shouldBeAKey 183 | } 184 | if !shouldBeAKey { 185 | l.pairs = append(l.pairs, toPair(MessageKey, key)) 186 | } 187 | return l 188 | } 189 | 190 | // Reset logger values added after last Log() call. It keeps context untouched. 191 | func (l *Logger) Reset() *Logger { 192 | l.pairs = nil 193 | return l 194 | } 195 | -------------------------------------------------------------------------------- /convertor_test.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | /* 4 | Copyright (c) 2016-2018, Alexander I.Grafov 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of kvlog nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ॐ तारे तुत्तारे तुरे स्व 33 | 34 | All tests consists of three parts: 35 | 36 | - arrange structures and initialize objects for use in tests 37 | - act on testing object 38 | - check and assert on results 39 | 40 | These parts separated by empty lines in each test function. 41 | */ 42 | 43 | import ( 44 | "bytes" 45 | "strings" 46 | "testing" 47 | "time" 48 | ) 49 | 50 | // Test of non default value of FloatFormat global var. 51 | // It should format the value accordingly with selected format ('f' in this test). 52 | func TestConvertor_NonDefaultFloatFormatPass_Logfmt(t *testing.T) { 53 | output := bytes.NewBufferString("") 54 | log := New() 55 | original := FloatFormat 56 | FloatFormat = 'f' 57 | out := SinkTo(output, AsLogfmt()).Start() 58 | 59 | log.Log("key", 3.14159265) 60 | 61 | out.Close() 62 | if strings.TrimSpace(output.String()) != `key=3.14159265` { 63 | println(output.String()) 64 | t.Fail() 65 | } 66 | FloatFormat = original 67 | } 68 | 69 | // Test of non default value of TimeLayout global var. 70 | // It should format the value accordingly with selected format. 71 | func TestConvertor_NonDefaultTimeLayoutPass_Logfmt(t *testing.T) { 72 | output := bytes.NewBufferString("") 73 | log := New() 74 | original := TimeLayout 75 | TimeLayout = time.RFC822 76 | now := time.Now() 77 | nowString := now.Format(time.RFC822) 78 | out := SinkTo(output, AsLogfmt()).Start() 79 | 80 | log.Log("key", now) 81 | 82 | out.Close() 83 | if strings.TrimSpace(output.String()) != `key=`+nowString { 84 | println(output.String()) 85 | t.Fail() 86 | } 87 | TimeLayout = original 88 | } 89 | 90 | func TestConvertor_LogByteType_Logfmt(t *testing.T) { 91 | output := bytes.NewBufferString("") 92 | log := New() 93 | out := SinkTo(output, AsLogfmt()).Start() 94 | 95 | log.Log("the key", []byte("the sample byte sequence...")) 96 | 97 | out.Close() 98 | if strings.TrimSpace(output.String()) != `"the key"="the sample byte sequence..."` { 99 | println(output.String()) 100 | t.Fail() 101 | } 102 | } 103 | 104 | func TestConvertor_LogBoolType_Logfmt(t *testing.T) { 105 | output := bytes.NewBufferString("") 106 | log := New() 107 | out := SinkTo(output, AsLogfmt()).Start() 108 | 109 | log.Log("true", false) 110 | 111 | out.Close() 112 | if strings.TrimSpace(output.String()) != `true=false` { 113 | println(output.String()) 114 | t.Fail() 115 | } 116 | } 117 | 118 | func TestConvertor_LogInt8Type_Logfmt(t *testing.T) { 119 | output := bytes.NewBufferString("") 120 | log := New() 121 | out := SinkTo(output, AsLogfmt()).Start() 122 | 123 | log.Log("1", int8(2)) 124 | 125 | out.Close() 126 | if strings.TrimSpace(output.String()) != `1=2` { 127 | println(output.String()) 128 | t.Fail() 129 | } 130 | } 131 | 132 | func TestConvertor_LogInt16Type_Logfmt(t *testing.T) { 133 | output := bytes.NewBufferString("") 134 | log := New() 135 | out := SinkTo(output, AsLogfmt()).Start() 136 | 137 | log.Log("1", int16(2)) 138 | 139 | out.Close() 140 | if strings.TrimSpace(output.String()) != `1=2` { 141 | println(output.String()) 142 | t.Fail() 143 | } 144 | } 145 | 146 | func TestConvertor_LogInt32Type_Logfmt(t *testing.T) { 147 | output := bytes.NewBufferString("") 148 | log := New() 149 | out := SinkTo(output, AsLogfmt()).Start() 150 | 151 | log.Log("1", int32(2)) 152 | 153 | out.Close() 154 | if strings.TrimSpace(output.String()) != `1=2` { 155 | println(output.String()) 156 | t.Fail() 157 | } 158 | } 159 | 160 | func TestConvertor_LogIntType_Logfmt(t *testing.T) { 161 | output := bytes.NewBufferString("") 162 | log := New() 163 | out := SinkTo(output, AsLogfmt()).Start() 164 | 165 | log.Log("1", 2) 166 | 167 | out.Close() 168 | if strings.TrimSpace(output.String()) != `1=2` { 169 | println(output.String()) 170 | t.Fail() 171 | } 172 | } 173 | 174 | func TestConvertor_LogInt64Type_Logfmt(t *testing.T) { 175 | output := bytes.NewBufferString("") 176 | log := New() 177 | out := SinkTo(output, AsLogfmt()).Start() 178 | 179 | log.Log("1", int64(2)) 180 | 181 | out.Close() 182 | if strings.TrimSpace(output.String()) != `1=2` { 183 | println(output.String()) 184 | t.Fail() 185 | } 186 | } 187 | 188 | func TestConvertor_LogUint8Type_Logfmt(t *testing.T) { 189 | output := bytes.NewBufferString("") 190 | log := New() 191 | out := SinkTo(output, AsLogfmt()).Start() 192 | 193 | log.Log("1", uint8(2)) 194 | 195 | out.Close() 196 | if strings.TrimSpace(output.String()) != `1=2` { 197 | println(output.String()) 198 | t.Fail() 199 | } 200 | } 201 | 202 | func TestConvertor_LogUint16Type_Logfmt(t *testing.T) { 203 | output := bytes.NewBufferString("") 204 | log := New() 205 | out := SinkTo(output, AsLogfmt()).Start() 206 | 207 | log.Log("1", uint16(2)) 208 | 209 | out.Close() 210 | if strings.TrimSpace(output.String()) != `1=2` { 211 | println(output.String()) 212 | t.Fail() 213 | } 214 | } 215 | 216 | func TestConvertor_LogUint32Type_Logfmt(t *testing.T) { 217 | output := bytes.NewBufferString("") 218 | log := New() 219 | out := SinkTo(output, AsLogfmt()).Start() 220 | 221 | log.Log("1", uint32(2)) 222 | 223 | out.Close() 224 | if strings.TrimSpace(output.String()) != `1=2` { 225 | println(output.String()) 226 | t.Fail() 227 | } 228 | } 229 | 230 | func TestConvertor_LogUintType_Logfmt(t *testing.T) { 231 | output := bytes.NewBufferString("") 232 | log := New() 233 | out := SinkTo(output, AsLogfmt()).Start() 234 | 235 | log.Log("1", uint(2)) 236 | 237 | out.Close() 238 | if strings.TrimSpace(output.String()) != `1=2` { 239 | println(output.String()) 240 | t.Fail() 241 | } 242 | } 243 | 244 | func TestConvertor_LogUint64Type_Logfmt(t *testing.T) { 245 | output := bytes.NewBufferString("") 246 | log := New() 247 | out := SinkTo(output, AsLogfmt()).Start() 248 | 249 | log.Log("1", uint64(2)) 250 | 251 | out.Close() 252 | if strings.TrimSpace(output.String()) != `1=2` { 253 | println(output.String()) 254 | t.Fail() 255 | } 256 | } 257 | 258 | func TestConvertor_LogFloat32Type_Logfmt(t *testing.T) { 259 | output := bytes.NewBufferString("") 260 | log := New() 261 | out := SinkTo(output, AsLogfmt()).Start() 262 | 263 | log.Log("pi", float32(3.14159265)) 264 | 265 | out.Close() 266 | if strings.TrimSpace(output.String()) != `pi=3.1415927e+00` { 267 | println(output.String()) 268 | t.Fail() 269 | } 270 | } 271 | 272 | func TestConvertor_LogFloat64Type_Logfmt(t *testing.T) { 273 | output := bytes.NewBufferString("") 274 | log := New() 275 | out := SinkTo(output, AsLogfmt()).Start() 276 | 277 | log.Log("pi", 3.14159265359) 278 | 279 | out.Close() 280 | if strings.TrimSpace(output.String()) != `pi=3.14159265359e+00` { 281 | println(output.String()) 282 | t.Fail() 283 | } 284 | } 285 | 286 | func TestConvertor_LogNil(t *testing.T) { 287 | output := bytes.NewBufferString("") 288 | log := New() 289 | out := SinkTo(output, AsLogfmt()).Start() 290 | 291 | log.Log("key", nil) 292 | 293 | out.Close() 294 | if strings.TrimSpace(output.String()) != `key=""` { 295 | println(output.String()) 296 | t.Fail() 297 | } 298 | } 299 | 300 | func TestConvertor_LogValueWithoutKey(t *testing.T) { 301 | output := bytes.NewBufferString("") 302 | log := New() 303 | out := SinkTo(output, AsLogfmt()).Start() 304 | 305 | log.Log("just a single value") 306 | 307 | out.Close() 308 | if strings.TrimSpace(output.String()) != `message="just a single value"` { 309 | println(output.String()) 310 | t.Fail() 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /logger_json_test.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | /* 4 | Copyright (c) 2016-2019, Alexander I.Grafov 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of kvlog nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ॐ तारे तुत्तारे तुरे स्व 33 | */ 34 | 35 | import ( 36 | "bytes" 37 | "fmt" 38 | "strings" 39 | "testing" 40 | "time" 41 | ) 42 | 43 | /* All tests consists of three parts: 44 | 45 | - arrange structures and initialize objects for use in tests 46 | - act on testing object 47 | - check and assert on results 48 | 49 | These parts separated by empty lines in each test function. 50 | */ 51 | 52 | // Test logging of string value. 53 | func TestLogger_LogStringValue_JSON(t *testing.T) { 54 | output := bytes.NewBufferString("") 55 | log := New() 56 | out := SinkTo(output, AsJSON()).Start() 57 | defer out.Close() 58 | 59 | log.Log("k", "The sample string with a lot of spaces.") 60 | 61 | out.Flush() 62 | if strings.TrimSpace(output.String()) != `{"k":"The sample string with a lot of spaces.", }` { 63 | t.Fail() 64 | } 65 | } 66 | 67 | // Test logging of byte array. 68 | func TestLogger_LogBytesValue_JSON(t *testing.T) { 69 | output := bytes.NewBufferString("") 70 | log := New() 71 | out := SinkTo(output, AsJSON()).Start() 72 | defer out.Close() 73 | 74 | log.Log("k", []byte("The sample string with a lot of spaces.")) 75 | 76 | out.Flush() 77 | if strings.TrimSpace(output.String()) != `{"k":"The sample string with a lot of spaces.", }` { 78 | t.Fail() 79 | } 80 | } 81 | 82 | // Test logging of integer value. 83 | func TestLogger_LogIntValue_JSON(t *testing.T) { 84 | output := bytes.NewBufferString("") 85 | log := New() 86 | out := SinkTo(output, AsJSON()).Start() 87 | defer out.Close() 88 | 89 | log.Log("k", 123) 90 | 91 | out.Flush() 92 | if strings.TrimSpace(output.String()) != `{"k":123, }` { 93 | t.Fail() 94 | } 95 | } 96 | 97 | // Test logging of negative integer value. 98 | func TestLogger_LogNegativeIntValue_JSON(t *testing.T) { 99 | output := bytes.NewBufferString("") 100 | log := New() 101 | out := SinkTo(output, AsJSON()).Start() 102 | defer out.Close() 103 | 104 | log.Log("k", -123) 105 | 106 | out.Flush() 107 | if strings.TrimSpace(output.String()) != `{"k":-123, }` { 108 | t.Fail() 109 | } 110 | } 111 | 112 | // Test logging of float value in default (scientific) format. 113 | func TestLogger_LogFloatValue_JSON(t *testing.T) { 114 | output := bytes.NewBufferString("") 115 | log := New() 116 | out := SinkTo(output, AsJSON()).Start() 117 | defer out.Close() 118 | 119 | log.Log("k", 3.14159265359) 120 | 121 | out.Flush() 122 | if strings.TrimSpace(output.String()) != `{"k":3.14159265359e+00, }` { 123 | t.Fail() 124 | } 125 | } 126 | 127 | // Test logging of float value in fixed format. 128 | func TestLogger_LogFixedFloatValue_JSON(t *testing.T) { 129 | output := bytes.NewBufferString("") 130 | log := New() 131 | out := SinkTo(output, AsJSON()).Start() 132 | defer out.Close() 133 | 134 | FloatFormat = 'f' 135 | log.Log("k", 3.14159265359) 136 | 137 | out.Flush() 138 | if strings.TrimSpace(output.String()) != `{"k":3.14159265359, }` { 139 | t.Fail() 140 | } 141 | // Turn back to default format. 142 | FloatFormat = 'e' 143 | } 144 | 145 | // Test logging of boolean value. 146 | func TestLogger_LogBoolValue_JSON(t *testing.T) { 147 | output := bytes.NewBufferString("") 148 | log := New() 149 | out := SinkTo(output, AsJSON()).Start() 150 | defer out.Close() 151 | 152 | log.Log("k", true, "k2", false) 153 | 154 | out.Flush() 155 | if strings.TrimSpace(output.String()) != `{"k":true, "k2":false, }` { 156 | t.Fail() 157 | } 158 | } 159 | 160 | // Test logging of complex number. 161 | func TestLogger_LogComplexValue_JSON(t *testing.T) { 162 | output := bytes.NewBufferString("") 163 | log := New() 164 | out := SinkTo(output, AsJSON()).Start() 165 | defer out.Close() 166 | 167 | log.Log("k", .12345E+5i, "k2", 1.e+0i) 168 | 169 | out.Flush() 170 | if strings.TrimSpace(output.String()) != `{"k":(0.000000+12345.000000i), "k2":(0.000000+1.000000i), }` { 171 | t.Fail() 172 | } 173 | } 174 | 175 | // Test logging of time literal. 176 | func TestLogger_LogTimeValue_JSON(t *testing.T) { 177 | output := bytes.NewBufferString("") 178 | log := New() 179 | out := SinkTo(output, AsJSON()).Start() 180 | value := time.Now() 181 | valueString := value.Format(TimeLayout) 182 | defer out.Close() 183 | 184 | log.Log("k", value) 185 | 186 | out.Flush() 187 | expect := fmt.Sprintf(`{"k":"%s", }`, valueString) 188 | got := strings.TrimSpace(output.String()) 189 | if got != expect { 190 | t.Logf("expected %s got %v", expect, got) 191 | t.Fail() 192 | } 193 | } 194 | 195 | func TestLogger_LogIntKeyInvalid_JSON(t *testing.T) { 196 | output := bytes.NewBufferString("") 197 | log := New() 198 | out := SinkTo(output, AsJSON()).Start() 199 | defer out.Close() 200 | 201 | log.Log(123, 456) 202 | 203 | out.Flush() 204 | expect := `{"kiwi-error":"non a string type (int) for the key (123)", "message":456, }` 205 | got := strings.TrimSpace(output.String()) 206 | if got != expect { 207 | t.Logf("expected %s got %v", expect, got) 208 | t.Fail() 209 | } 210 | } 211 | 212 | // The logger accepts string keys. In other cases it complains about 213 | // the wrong key type. It also assumes that each key followed by the 214 | // value. 215 | func TestLogger_LogThreeIntKeysInvalid_JSON(t *testing.T) { 216 | output := bytes.NewBufferString("") 217 | log := New() 218 | out := SinkTo(output, AsJSON()).Start() 219 | defer out.Close() 220 | 221 | log.Log(123, 456, 789) 222 | 223 | out.Flush() 224 | expect := `{"kiwi-error":"non a string type (int) for the key (123)", "message":456, "kiwi-error":"non a string type (int) for the key (789)", }` 225 | got := strings.TrimSpace(output.String()) 226 | if got != expect { 227 | t.Logf("expected %s got %v", expect, got) 228 | t.Fail() 229 | } 230 | } 231 | 232 | // The logger accepts string keys. In other cases it complains about 233 | // the wrong key type. It also assumes that each key followed by the 234 | // value. 235 | func TestLogger_LogFourIntKeysInvalid_JSON(t *testing.T) { 236 | output := bytes.NewBufferString("") 237 | log := New() 238 | out := SinkTo(output, AsJSON()).Start() 239 | defer out.Close() 240 | 241 | log.Log(12, 34, 56, 78) 242 | 243 | out.Flush() 244 | expect := `{"kiwi-error":"non a string type (int) for the key (12)", "message":34, "kiwi-error":"non a string type (int) for the key (56)", "message":78, }` 245 | got := strings.TrimSpace(output.String()) 246 | if got != expect { 247 | t.Logf("expected %s got %v", expect, got) 248 | t.Fail() 249 | } 250 | } 251 | 252 | // Test chaining for Add() 253 | func TestLogger_AddMixChained_JSON(t *testing.T) { 254 | output := bytes.NewBufferString("") 255 | log := New() 256 | out := SinkTo(output, AsJSON()).Start() 257 | defer out.Close() 258 | 259 | log.Add("k", "value2").Add("k2", 123).Add("k3", 3.14159265359).Log() 260 | 261 | out.Flush() 262 | expect := `{"k":"value2", "k2":123, "k3":3.14159265359e+00, }` 263 | got := strings.TrimSpace(output.String()) 264 | if got != expect { 265 | t.Logf("expected %s got %v", expect, got) 266 | t.Fail() 267 | } 268 | } 269 | 270 | // Test log with the context value. 271 | func TestLogger_WithContextPassed_JSON(t *testing.T) { 272 | output := bytes.NewBufferString("") 273 | log := New() 274 | out := SinkTo(output, AsJSON()).Start() 275 | defer out.Close() 276 | 277 | log.With("key1", "value") 278 | log.Log("key2", "value") 279 | 280 | out.Flush() 281 | expect := `{"key1":"value", "key2":"value", }` 282 | got := strings.TrimSpace(output.String()) 283 | if got != expect { 284 | t.Logf("expected %s got %v", expect, got) 285 | t.Fail() 286 | } 287 | 288 | } 289 | 290 | // Test log with adding then removing the context. 291 | func TestLogger_WithoutContextPassed_JSON(t *testing.T) { 292 | output := bytes.NewBufferString("") 293 | log := New() 294 | out := SinkTo(output, AsJSON()).Start() 295 | defer out.Close() 296 | 297 | // add the context 298 | log.With("key1", "value") 299 | // add regular pair 300 | log.Add("key2", "value") 301 | // remove the context and flush the record 302 | log.Without("key1").Log() 303 | 304 | out.Flush() 305 | if strings.TrimSpace(output.String()) != `{"key2":"value", }` { 306 | t.Fail() 307 | } 308 | } 309 | 310 | // Test log with adding then reset the context. 311 | func TestLogger_ResetContext_JSON(t *testing.T) { 312 | output := bytes.NewBufferString("") 313 | log := New() 314 | out := SinkTo(output, AsJSON()).Start() 315 | defer out.Close() 316 | 317 | // add the context 318 | log.With("key1", "value") 319 | // add regular pair 320 | log.Add("key2", "value") 321 | // reset the context and flush the record 322 | log.ResetContext().Log() 323 | 324 | out.Flush() 325 | if strings.TrimSpace(output.String()) != `{"key2":"value", }` { 326 | t.Fail() 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | /* 4 | Copyright (c) 2016-2019, Alexander I.Grafov 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of kvlog nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ॐ तारे तुत्तारे तुरे स्व 33 | */ 34 | 35 | import ( 36 | "bytes" 37 | "fmt" 38 | "strings" 39 | "testing" 40 | "time" 41 | ) 42 | 43 | /* All tests consists of three parts: 44 | 45 | - arrange structures and initialize objects for use in tests 46 | - act on testing object 47 | - check and assert on results 48 | 49 | These parts separated by empty lines in each test function. 50 | */ 51 | 52 | // Test logging of string value. 53 | func TestLogger_LogStringValue_Logfmt(t *testing.T) { 54 | output := bytes.NewBufferString("") 55 | log := New() 56 | out := SinkTo(output, AsLogfmt()).Start() 57 | defer out.Close() 58 | 59 | log.Log("k", "The sample string with a lot of spaces.") 60 | 61 | if strings.TrimSpace(output.String()) != `k="The sample string with a lot of spaces."` { 62 | t.Fail() 63 | } 64 | } 65 | 66 | // Test logging of byte array. 67 | func TestLogger_LogBytesValue_Logfmt(t *testing.T) { 68 | output := bytes.NewBufferString("") 69 | log := New() 70 | out := SinkTo(output, AsLogfmt()).Start() 71 | defer out.Close() 72 | 73 | log.Log("k", []byte("The sample string with a lot of spaces.")) 74 | 75 | 76 | if strings.TrimSpace(output.String()) != `k="The sample string with a lot of spaces."` { 77 | t.Fail() 78 | } 79 | } 80 | 81 | // Test logging of integer value. 82 | func TestLogger_LogIntValue_Logfmt(t *testing.T) { 83 | output := bytes.NewBufferString("") 84 | log := New() 85 | out := SinkTo(output, AsLogfmt()).Start() 86 | defer out.Close() 87 | 88 | log.Log("k", 123) 89 | 90 | 91 | if strings.TrimSpace(output.String()) != "k=123" { 92 | t.Fail() 93 | } 94 | } 95 | 96 | // Test logging of negative integer value. 97 | func TestLogger_LogNegativeIntValue_Logfmt(t *testing.T) { 98 | output := bytes.NewBufferString("") 99 | log := New() 100 | out := SinkTo(output, AsLogfmt()).Start() 101 | defer out.Close() 102 | 103 | log.Log("k", -123) 104 | 105 | 106 | if strings.TrimSpace(output.String()) != "k=-123" { 107 | t.Fail() 108 | } 109 | } 110 | 111 | // Test logging of float value in default (scientific) format. 112 | func TestLogger_LogFloatValue_Logfmt(t *testing.T) { 113 | output := bytes.NewBufferString("") 114 | log := New() 115 | out := SinkTo(output, AsLogfmt()).Start() 116 | defer out.Close() 117 | 118 | log.Log("k", 3.14159265359) 119 | 120 | 121 | if strings.TrimSpace(output.String()) != "k=3.14159265359e+00" { 122 | t.Fail() 123 | } 124 | } 125 | 126 | // Test logging of float value in fixed format. 127 | func TestLogger_LogFixedFloatValue_Logfmt(t *testing.T) { 128 | output := bytes.NewBufferString("") 129 | log := New() 130 | out := SinkTo(output, AsLogfmt()).Start() 131 | defer out.Close() 132 | 133 | FloatFormat = 'f' 134 | log.Log("k", 3.14159265359) 135 | 136 | 137 | if strings.TrimSpace(output.String()) != "k=3.14159265359" { 138 | t.Fail() 139 | } 140 | // Turn back to default format. 141 | FloatFormat = 'e' 142 | } 143 | 144 | // Test logging of boolean value. 145 | func TestLogger_LogBoolValue_Logfmt(t *testing.T) { 146 | output := bytes.NewBufferString("") 147 | log := New() 148 | out := SinkTo(output, AsLogfmt()).Start() 149 | defer out.Close() 150 | 151 | log.Log("k", true, "k2", false) 152 | 153 | 154 | if strings.TrimSpace(output.String()) != "k=true k2=false" { 155 | t.Fail() 156 | } 157 | } 158 | 159 | // Test logging of complex number. 160 | func TestLogger_LogComplexValue_Logfmt(t *testing.T) { 161 | output := bytes.NewBufferString("") 162 | log := New() 163 | out := SinkTo(output, AsLogfmt()).Start() 164 | defer out.Close() 165 | 166 | log.Log("k", .12345E+5i, "k2", 1.e+0i) 167 | 168 | 169 | if strings.TrimSpace(output.String()) != "k=(0.000000+12345.000000i) k2=(0.000000+1.000000i)" { 170 | t.Fail() 171 | } 172 | } 173 | 174 | // Test logging of time literal. 175 | func TestLogger_LogTimeValue_Logfmt(t *testing.T) { 176 | output := bytes.NewBufferString("") 177 | log := New() 178 | out := SinkTo(output, AsLogfmt()).Start() 179 | value := time.Now() 180 | valueString := value.Format(TimeLayout) 181 | defer out.Close() 182 | 183 | log.Log("k", value) 184 | 185 | 186 | if strings.TrimSpace(output.String()) != fmt.Sprintf("k=%s", valueString) { 187 | t.Fail() 188 | } 189 | } 190 | 191 | func TestLogger_LogIntKeyInvalid_Logfmt(t *testing.T) { 192 | output := bytes.NewBufferString("") 193 | log := New() 194 | out := SinkTo(output, AsLogfmt()).Start() 195 | defer out.Close() 196 | 197 | log.Log(123, 456) 198 | 199 | 200 | expected := `kiwi-error="non a string type (int) for the key (123)" message=456` 201 | if strings.TrimSpace(output.String()) != expected { 202 | t.Logf("expected %s got %v", expected, output.String()) 203 | t.Fail() 204 | } 205 | } 206 | 207 | // The logger accepts string keys. In other cases it complains about 208 | // the wrong key type. It also assumes that each key followed by the 209 | // value. 210 | func TestLogger_LogThreeIntKeysInvalid_Logfmt(t *testing.T) { 211 | output := bytes.NewBufferString("") 212 | log := New() 213 | out := SinkTo(output, AsLogfmt()).Start() 214 | defer out.Close() 215 | 216 | log.Log(123, 456, 789) 217 | 218 | 219 | expected := `kiwi-error="non a string type (int) for the key (123)" message=456 kiwi-error="non a string type (int) for the key (789)"` 220 | if strings.TrimSpace(output.String()) != expected { 221 | t.Logf("expected %s got %v", expected, output.String()) 222 | t.Fail() 223 | } 224 | } 225 | 226 | // The logger accepts string keys. In other cases it complains about 227 | // the wrong key type. It also assumes that each key followed by the 228 | // value. 229 | func TestLogger_LogFourIntKeysInvalid_Logfmt(t *testing.T) { 230 | output := bytes.NewBufferString("") 231 | log := New() 232 | out := SinkTo(output, AsLogfmt()).Start() 233 | defer out.Close() 234 | 235 | log.Log(12, 34, 56, 78) 236 | 237 | 238 | expected := `kiwi-error="non a string type (int) for the key (12)" message=34 kiwi-error="non a string type (int) for the key (56)" message=78` 239 | if strings.TrimSpace(output.String()) != expected { 240 | t.Logf("expected %s got %v", expected, output.String()) 241 | t.Fail() 242 | } 243 | } 244 | 245 | // Test chaining for Add() 246 | func TestLogger_AddMixChained_Logfmt(t *testing.T) { 247 | output := bytes.NewBufferString("") 248 | log := New() 249 | out := SinkTo(output, AsLogfmt()).Start() 250 | defer out.Close() 251 | 252 | log.Add("k", "value2").Add("k2", 123).Add("k3", 3.14159265359).Log() 253 | 254 | 255 | expected := `k="value2" k2=123 k3=3.14159265359e+00` 256 | if strings.TrimSpace(output.String()) != expected { 257 | t.Logf("expected %s got %v", expected, output.String()) 258 | t.Fail() 259 | } 260 | } 261 | 262 | // Test chaining for Add() 263 | func TestLogger_AddSameKeysChained_Logfmt(t *testing.T) { 264 | output := bytes.NewBufferString("") 265 | log := New() 266 | out := SinkTo(output, AsLogfmt()).Start() 267 | defer out.Close() 268 | 269 | log.Add("k", "value").Add("k", 123).Log("k", "another") 270 | 271 | 272 | expected := `k="value" k=123 k="another"` 273 | if strings.TrimSpace(output.String()) != expected { 274 | t.Logf("expected %s got %v", expected, output.String()) 275 | t.Fail() 276 | } 277 | } 278 | 279 | // Test log with the context value. 280 | func TestLogger_WithContextPassed_Logfmt(t *testing.T) { 281 | output := bytes.NewBufferString("") 282 | log := New() 283 | out := SinkTo(output, AsLogfmt()).Start() 284 | defer out.Close() 285 | 286 | log.With("key1", "value") 287 | log.Log("key2", "value") 288 | 289 | 290 | if strings.TrimSpace(output.String()) != `key1="value" key2="value"` { 291 | t.Fail() 292 | } 293 | } 294 | 295 | // Test log with adding then removing the context. 296 | func TestLogger_WithoutContextPassed_Logfmt(t *testing.T) { 297 | output := bytes.NewBufferString("") 298 | log := New() 299 | out := SinkTo(output, AsLogfmt()).Start() 300 | defer out.Close() 301 | 302 | // add the context 303 | log.With("key1", "value") 304 | // add regular pair 305 | log.Add("key2", "value") 306 | // remove the context and flush the record 307 | log.Without("key1").Log() 308 | 309 | 310 | if strings.TrimSpace(output.String()) != `key2="value"` { 311 | t.Fail() 312 | } 313 | } 314 | 315 | // Test log with adding then reset the context. 316 | func TestLogger_ResetContext_Logfmt(t *testing.T) { 317 | output := bytes.NewBufferString("") 318 | log := New() 319 | out := SinkTo(output, AsLogfmt()).Start() 320 | defer out.Close() 321 | 322 | // add the context 323 | log.With("key1", "value") 324 | // add regular pair 325 | log.Add("key2", "value") 326 | // reset the context and flush the record 327 | log.ResetContext().Log() 328 | 329 | 330 | if strings.TrimSpace(output.String()) != `key2="value"` { 331 | t.Fail() 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /race_test.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | /* 4 | Copyright (c) 2016-2019, Alexander I.Grafov 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of kvlog nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ॐ तारे तुत्तारे तुरे स्व 33 | */ 34 | 35 | import ( 36 | "bytes" 37 | "sync" 38 | "testing" 39 | ) 40 | 41 | /* All tests consists of three parts: 42 | 43 | - arrange structures and initialize objects for use in tests 44 | - act on testing object 45 | - check and assert on results 46 | 47 | These parts separated by empty lines in each test function. 48 | */ 49 | 50 | // Test logging of string value. 51 | func TestRace_NewFromGlobal_Logfmt(t *testing.T) { 52 | output := bytes.NewBufferString("") 53 | out := SinkTo(output, AsLogfmt()).Start() 54 | defer out.Close() 55 | 56 | for i := 0; i < 100; i++ { 57 | go func(i int) { 58 | log := New() 59 | log.Log("k", "The sample string.", "instance", i) 60 | }(i) 61 | } 62 | Log("k", "The sample string.", "instance", 0) 63 | } 64 | 65 | // Test logging of string value. 66 | func TestRace_ForkFromGlobal_Logfmt(t *testing.T) { 67 | output := bytes.NewBufferString("") 68 | out := SinkTo(output, AsLogfmt()).Start() 69 | defer out.Close() 70 | 71 | for i := 0; i < 100; i++ { 72 | go func(i int) { 73 | log := Fork() 74 | log.Log("k", "The sample string.", "instance", i) 75 | }(i) 76 | } 77 | Log("k", "The sample string.", "instance", 0) 78 | } 79 | 80 | // Test logging of string value. 81 | func TestRace_Fork_Logfmt(t *testing.T) { 82 | output := bytes.NewBufferString("") 83 | out := SinkTo(output, AsLogfmt()).Start() 84 | defer out.Close() 85 | var ( 86 | wg sync.WaitGroup 87 | ) 88 | 89 | for i := 0; i < 100; i++ { 90 | wg.Add(1) 91 | go func(i int) { 92 | log := Fork() 93 | log.Log("k", "The sample string.", "instance", i) 94 | wg.Done() 95 | }(i) 96 | } 97 | Log("k", "The sample string.", "instance", 0) 98 | wg.Wait() 99 | } 100 | 101 | // Test logging of string value. 102 | func TestRace_ForkFork(t *testing.T) { 103 | output := bytes.NewBufferString("") 104 | out := SinkTo(output, AsLogfmt()).Start() 105 | defer out.Close() 106 | var ( 107 | wg sync.WaitGroup 108 | ) 109 | 110 | for i := 0; i < 100; i++ { 111 | wg.Add(1) 112 | go func(i int) { 113 | log := Fork().Fork() 114 | log.Log("k", "The sample string.", "instance", i) 115 | wg.Done() 116 | }(i) 117 | } 118 | Log("k", "The sample string.", "instance", 0) 119 | wg.Wait() 120 | } 121 | 122 | // Test logging of string value. 123 | func TestRace_ForkInside_Logfmt(t *testing.T) { 124 | output := bytes.NewBufferString("") 125 | out := SinkTo(output, AsLogfmt()).Start() 126 | defer out.Close() 127 | var ( 128 | wg sync.WaitGroup 129 | log = Fork() 130 | ) 131 | log.With("logger", "global") 132 | log.Log("k", "The sample string.", "instance", 0) 133 | 134 | for i := 0; i < 100; i++ { 135 | wg.Add(1) 136 | go func(i int, log *Logger) { 137 | l := log.Fork().With("logger", i) 138 | l.Log("k", "The sample string.", "instance", i) 139 | wg.Done() 140 | }(i, log) 141 | } 142 | log.Log("k", "The sample string.", "instance", 0) 143 | wg.Wait() 144 | } 145 | 146 | // // Test logging of byte array. 147 | // func TestLogger_LogBytesValue_Logfmt(t *testing.T) { 148 | // output := bytes.NewBufferString("") 149 | // log := New() 150 | // out := // defer out.Close() 151 | 152 | // log.Log("k", []byte("The sample string with a lot of spaces.")) 153 | 154 | // 155 | // if strings.TrimSpace(output.String()) != `k="The sample string with a lot of spaces."` { 156 | // t.Fail() 157 | // } 158 | // } 159 | 160 | // // Test logging of integer value. 161 | // func TestLogger_LogIntValue_Logfmt(t *testing.T) { 162 | // output := bytes.NewBufferString("") 163 | // log := New() 164 | // out := SinkTo(output, AsLogfmt()).Start() 165 | // defer out.Close() 166 | 167 | // log.Log("k", 123) 168 | 169 | // 170 | // if strings.TrimSpace(output.String()) != "k=123" { 171 | // t.Fail() 172 | // } 173 | // } 174 | 175 | // // Test logging of negative integer value. 176 | // func TestLogger_LogNegativeIntValue_Logfmt(t *testing.T) { 177 | // output := bytes.NewBufferString("") 178 | // log := New() 179 | // out := SinkTo(output, AsLogfmt()).Start() 180 | // defer out.Close() 181 | 182 | // log.Log("k", -123) 183 | 184 | // 185 | // if strings.TrimSpace(output.String()) != "k=-123" { 186 | // t.Fail() 187 | // } 188 | // } 189 | 190 | // // Test logging of float value in default (scientific) format. 191 | // func TestLogger_LogFloatValue_Logfmt(t *testing.T) { 192 | // output := bytes.NewBufferString("") 193 | // log := New() 194 | // out := SinkTo(output, AsLogfmt()).Start() 195 | // defer out.Close() 196 | 197 | // log.Log("k", 3.14159265359) 198 | 199 | // 200 | // if strings.TrimSpace(output.String()) != "k=3.14159265359e+00" { 201 | // t.Fail() 202 | // } 203 | // } 204 | 205 | // // Test logging of float value in fixed format. 206 | // func TestLogger_LogFixedFloatValue_Logfmt(t *testing.T) { 207 | // output := bytes.NewBufferString("") 208 | // log := New() 209 | // out := SinkTo(output, AsLogfmt()).Start() 210 | // defer out.Close() 211 | 212 | // FloatFormat = 'f' 213 | // log.Log("k", 3.14159265359) 214 | 215 | // 216 | // if strings.TrimSpace(output.String()) != "k=3.14159265359" { 217 | // t.Fail() 218 | // } 219 | // // Turn back to default format. 220 | // FloatFormat = 'e' 221 | // } 222 | 223 | // // Test logging of boolean value. 224 | // func TestLogger_LogBoolValue_Logfmt(t *testing.T) { 225 | // output := bytes.NewBufferString("") 226 | // log := New() 227 | // out := SinkTo(output, AsLogfmt()).Start() 228 | // defer out.Close() 229 | 230 | // log.Log("k", true, "k2", false) 231 | 232 | // 233 | // if strings.TrimSpace(output.String()) != "k=true k2=false" { 234 | // t.Fail() 235 | // } 236 | // } 237 | 238 | // // Test logging of complex number. 239 | // func TestLogger_LogComplexValue_Logfmt(t *testing.T) { 240 | // output := bytes.NewBufferString("") 241 | // log := New() 242 | // out := SinkTo(output, AsLogfmt()).Start() 243 | // defer out.Close() 244 | 245 | // log.Log("k", .12345E+5i, "k2", 1.e+0i) 246 | 247 | // 248 | // if strings.TrimSpace(output.String()) != "k=(0.000000+12345.000000i) k2=(0.000000+1.000000i)" { 249 | // t.Fail() 250 | // } 251 | // } 252 | 253 | // // Test logging of time literal. 254 | // func TestLogger_LogTimeValue_Logfmt(t *testing.T) { 255 | // output := bytes.NewBufferString("") 256 | // log := New() 257 | // out := SinkTo(output, AsLogfmt()).Start() 258 | // value := time.Now() 259 | // valueString := value.Format(TimeLayout) 260 | // defer out.Close() 261 | 262 | // log.Log("k", value) 263 | 264 | // 265 | // if strings.TrimSpace(output.String()) != fmt.Sprintf("k=%s", valueString) { 266 | // t.Fail() 267 | // } 268 | // } 269 | 270 | // func TestLogger_LogIntKeyInvalid_Logfmt(t *testing.T) { 271 | // output := bytes.NewBufferString("") 272 | // log := New() 273 | // out := SinkTo(output, AsLogfmt()).Start() 274 | // defer out.Close() 275 | 276 | // log.Log(123, 456) 277 | 278 | // 279 | // expected := `kiwi-error="non a string type (int) for the key (123)" message=456` 280 | // if strings.TrimSpace(output.String()) != expected { 281 | // t.Logf("expected %s got %v", expected, output.String()) 282 | // t.Fail() 283 | // } 284 | // } 285 | 286 | // // The logger accepts string keys. In other cases it complains about 287 | // // the wrong key type. It also assumes that each key followed by the 288 | // // value. 289 | // func TestLogger_LogThreeIntKeysInvalid_Logfmt(t *testing.T) { 290 | // output := bytes.NewBufferString("") 291 | // log := New() 292 | // out := SinkTo(output, AsLogfmt()).Start() 293 | // defer out.Close() 294 | 295 | // log.Log(123, 456, 789) 296 | 297 | // 298 | // expected := `kiwi-error="non a string type (int) for the key (123)" message=456 kiwi-error="non a string type (int) for the key (789)"` 299 | // if strings.TrimSpace(output.String()) != expected { 300 | // t.Logf("expected %s got %v", expected, output.String()) 301 | // t.Fail() 302 | // } 303 | // } 304 | 305 | // // The logger accepts string keys. In other cases it complains about 306 | // // the wrong key type. It also assumes that each key followed by the 307 | // // value. 308 | // func TestLogger_LogFourIntKeysInvalid_Logfmt(t *testing.T) { 309 | // output := bytes.NewBufferString("") 310 | // log := New() 311 | // out := SinkTo(output, AsLogfmt()).Start() 312 | // defer out.Close() 313 | 314 | // log.Log(12, 34, 56, 78) 315 | 316 | // 317 | // expected := `kiwi-error="non a string type (int) for the key (12)" message=34 kiwi-error="non a string type (int) for the key (56)" message=78` 318 | // if strings.TrimSpace(output.String()) != expected { 319 | // t.Logf("expected %s got %v", expected, output.String()) 320 | // t.Fail() 321 | // } 322 | // } 323 | 324 | // // Test chaining for Add() 325 | // func TestLogger_AddMixChained_Logfmt(t *testing.T) { 326 | // output := bytes.NewBufferString("") 327 | // log := New() 328 | // out := SinkTo(output, AsLogfmt()).Start() 329 | // defer out.Close() 330 | 331 | // log.Add("k", "value2").Add("k2", 123).Add("k3", 3.14159265359).Log() 332 | 333 | // 334 | // expected := `k="value2" k2=123 k3=3.14159265359e+00` 335 | // if strings.TrimSpace(output.String()) != expected { 336 | // t.Logf("expected %s got %v", expected, output.String()) 337 | // t.Fail() 338 | // } 339 | // } 340 | 341 | // // Test log with the context value. 342 | // func TestLogger_WithContextPassed_Logfmt(t *testing.T) { 343 | // output := bytes.NewBufferString("") 344 | // log := New() 345 | // out := SinkTo(output, AsLogfmt()).Start() 346 | // defer out.Close() 347 | 348 | // log.With("key1", "value") 349 | // log.Log("key2", "value") 350 | 351 | // 352 | // if strings.TrimSpace(output.String()) != `key1="value" key2="value"` { 353 | // t.Fail() 354 | // } 355 | // } 356 | 357 | // // Test log with adding then removing the context. 358 | // func TestLogger_WithoutContextPassed_Logfmt(t *testing.T) { 359 | // output := bytes.NewBufferString("") 360 | // log := New() 361 | // out := SinkTo(output, AsLogfmt()).Start() 362 | // defer out.Close() 363 | 364 | // // add the context 365 | // log.With("key1", "value") 366 | // // add regular pair 367 | // log.Add("key2", "value") 368 | // // remove the context and flush the record 369 | // log.Without("key1").Log() 370 | 371 | // 372 | // if strings.TrimSpace(output.String()) != `key2="value"` { 373 | // t.Fail() 374 | // } 375 | // } 376 | 377 | // // Test log with adding then reset the context. 378 | // func TestLogger_ResetContext_Logfmt(t *testing.T) { 379 | // output := bytes.NewBufferString("") 380 | // log := New() 381 | // out := SinkTo(output, AsLogfmt()).Start() 382 | // defer out.Close() 383 | 384 | // // add the context 385 | // log.With("key1", "value") 386 | // // add regular pair 387 | // log.Add("key2", "value") 388 | // // reset the context and flush the record 389 | // log.ResetContext().Log() 390 | 391 | // 392 | // if strings.TrimSpace(output.String()) != `key2="value"` { 393 | // t.Fail() 394 | // } 395 | // } 396 | -------------------------------------------------------------------------------- /sink.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | // This file consists of Sink related structures and functions. 4 | // Outputs accepts incoming log records from Loggers, check them with filters 5 | // and write to output streams if checks passed. 6 | 7 | /* Copyright (c) 2016-2019, Alexander I.Grafov 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | * Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 16 | * Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | 20 | * Neither the name of kvlog nor the names of its 21 | contributors may be used to endorse or promote products derived from 22 | this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | ॐ तारे तुत्तारे तुरे स्व */ 36 | 37 | import ( 38 | "io" 39 | "sync" 40 | "sync/atomic" 41 | "time" 42 | ) 43 | 44 | // States of the sink. 45 | const ( 46 | sinkClosed int32 = iota - 1 47 | sinkStopped 48 | sinkActive 49 | ) 50 | 51 | // Sinks accepts records through the chanels. 52 | // Each sink has its own channel. 53 | var collector struct { 54 | sync.RWMutex 55 | sinks []*Sink 56 | } 57 | 58 | type ( 59 | // Sink used for filtering incoming log records from all logger instances 60 | // and decides how to filter them. Each output wraps its own io.Writer. 61 | // Sink methods are safe for concurrent usage. 62 | Sink struct { 63 | In chan chain 64 | close chan struct{} 65 | writer io.Writer 66 | format Formatter 67 | state *int32 68 | 69 | sync.RWMutex 70 | positiveFilters map[string]Filter 71 | negativeFilters map[string]Filter 72 | hiddenKeys map[string]bool 73 | } 74 | chain struct { 75 | wg *sync.WaitGroup 76 | pairs []*Pair 77 | } 78 | ) 79 | 80 | // SinkTo creates a new sink for an arbitrary number of loggers. 81 | // There are any number of sinks may be created for saving incoming log 82 | // records to different places. 83 | // The sink requires explicit start with Start() before usage. 84 | // That allows firstly setup filters before sink will really accept any records. 85 | func SinkTo(w io.Writer, fn Formatter) *Sink { 86 | collector.RLock() 87 | for i, sink := range collector.sinks { 88 | if sink.writer == w { 89 | collector.sinks[i].format = fn 90 | collector.RUnlock() 91 | return collector.sinks[i] 92 | } 93 | } 94 | collector.RUnlock() 95 | var ( 96 | state = sinkStopped 97 | sink = &Sink{ 98 | In: make(chan chain, 16), 99 | close: make(chan struct{}), 100 | format: fn, 101 | state: &state, 102 | writer: w, 103 | positiveFilters: make(map[string]Filter), 104 | negativeFilters: make(map[string]Filter), 105 | hiddenKeys: make(map[string]bool), 106 | } 107 | ) 108 | collector.Lock() 109 | collector.sinks = append(collector.sinks, sink) 110 | collector.Unlock() 111 | go processSink(sink) 112 | return sink 113 | } 114 | 115 | // HasKey sets restriction for records output. 116 | // Only the records WITH any of the keys will be passed to output. 117 | func (s *Sink) HasKey(keys ...string) *Sink { 118 | if atomic.LoadInt32(s.state) > sinkClosed { 119 | s.Lock() 120 | for _, key := range keys { 121 | s.positiveFilters[key] = &keyFilter{} 122 | delete(s.negativeFilters, key) 123 | } 124 | s.Unlock() 125 | } 126 | return s 127 | } 128 | 129 | // HasNotKey sets restriction for records output. 130 | // Only the records WITHOUT any of the keys will be passed to output. 131 | func (s *Sink) HasNotKey(keys ...string) *Sink { 132 | if atomic.LoadInt32(s.state) > sinkClosed { 133 | s.Lock() 134 | for _, key := range keys { 135 | s.negativeFilters[key] = &keyFilter{} 136 | delete(s.positiveFilters, key) 137 | } 138 | s.Unlock() 139 | } 140 | return s 141 | } 142 | 143 | // HasValue sets restriction for records output. 144 | // A record passed to output if the key equal one of any of the listed values. 145 | func (s *Sink) HasValue(key string, vals ...string) *Sink { 146 | if len(vals) == 0 { 147 | return s.HasKey(key) 148 | } 149 | if atomic.LoadInt32(s.state) > sinkClosed { 150 | s.Lock() 151 | s.positiveFilters[key] = &valsFilter{Vals: vals} 152 | delete(s.negativeFilters, key) 153 | s.Unlock() 154 | } 155 | return s 156 | } 157 | 158 | // HasNotValue sets restriction for records output. 159 | func (s *Sink) HasNotValue(key string, vals ...string) *Sink { 160 | if len(vals) == 0 { 161 | return s.HasNotKey(key) 162 | } 163 | if atomic.LoadInt32(s.state) > sinkClosed { 164 | s.Lock() 165 | s.negativeFilters[key] = &valsFilter{Vals: vals} 166 | delete(s.positiveFilters, key) 167 | s.Unlock() 168 | } 169 | return s 170 | } 171 | 172 | // Int64Range sets restriction for records output. 173 | func (s *Sink) Int64Range(key string, from, to int64) *Sink { 174 | if atomic.LoadInt32(s.state) > sinkClosed { 175 | s.Lock() 176 | delete(s.negativeFilters, key) 177 | s.positiveFilters[key] = &int64RangeFilter{From: from, To: to} 178 | s.Unlock() 179 | } 180 | return s 181 | } 182 | 183 | // Int64NotRange sets restriction for records output. 184 | func (s *Sink) Int64NotRange(key string, from, to int64) *Sink { 185 | if atomic.LoadInt32(s.state) > sinkClosed { 186 | s.Lock() 187 | delete(s.positiveFilters, key) 188 | s.negativeFilters[key] = &int64RangeFilter{From: from, To: to} 189 | s.Unlock() 190 | } 191 | return s 192 | } 193 | 194 | // Float64Range sets restriction for records output. 195 | func (s *Sink) Float64Range(key string, from, to float64) *Sink { 196 | if atomic.LoadInt32(s.state) > sinkClosed { 197 | s.Lock() 198 | delete(s.negativeFilters, key) 199 | s.positiveFilters[key] = &float64RangeFilter{From: from, To: to} 200 | s.Unlock() 201 | } 202 | return s 203 | } 204 | 205 | // Float64NotRange sets restriction for records output. 206 | func (s *Sink) Float64NotRange(key string, from, to float64) *Sink { 207 | if atomic.LoadInt32(s.state) > sinkClosed { 208 | s.Lock() 209 | delete(s.positiveFilters, key) 210 | s.negativeFilters[key] = &float64RangeFilter{From: from, To: to} 211 | s.Unlock() 212 | } 213 | return s 214 | } 215 | 216 | // TimeRange sets restriction for records output. 217 | func (s *Sink) TimeRange(key string, from, to time.Time) *Sink { 218 | if atomic.LoadInt32(s.state) > sinkClosed { 219 | s.Lock() 220 | delete(s.negativeFilters, key) 221 | s.positiveFilters[key] = &timeRangeFilter{From: from, To: to} 222 | s.Unlock() 223 | } 224 | return s 225 | } 226 | 227 | // TimeNotRange sets restriction for records output. 228 | func (s *Sink) TimeNotRange(key string, from, to time.Time) *Sink { 229 | if atomic.LoadInt32(s.state) > sinkClosed { 230 | s.Lock() 231 | delete(s.positiveFilters, key) 232 | s.negativeFilters[key] = &timeRangeFilter{From: from, To: to} 233 | s.Unlock() 234 | } 235 | return s 236 | } 237 | 238 | // WithFilter setup custom filtering function for values for a specific key. 239 | // Custom filter should realize Filter interface. All custom filters treated 240 | // as positive filters. So if the filter returns true then it will be passed. 241 | func (s *Sink) WithFilter(key string, customFilter Filter) *Sink { 242 | if atomic.LoadInt32(s.state) > sinkClosed { 243 | s.Lock() 244 | delete(s.negativeFilters, key) 245 | s.positiveFilters[key] = customFilter 246 | s.Unlock() 247 | } 248 | return s 249 | } 250 | 251 | // Reset all filters for the keys for the output. If no one key 252 | // provided it do global reset for all filters of the sink. 253 | func (s *Sink) Reset(keys ...string) *Sink { 254 | if atomic.LoadInt32(s.state) > sinkClosed { 255 | s.Lock() 256 | if len(keys) > 0 { 257 | for _, key := range keys { 258 | delete(s.positiveFilters, key) 259 | delete(s.negativeFilters, key) 260 | } 261 | } else { 262 | s.positiveFilters = make(map[string]Filter) 263 | s.negativeFilters = make(map[string]Filter) 264 | s.hiddenKeys = make(map[string]bool) 265 | } 266 | s.Unlock() 267 | } 268 | return s 269 | } 270 | 271 | // Hide keys from the output. Other keys in record will be displayed 272 | // but not hidden keys. 273 | func (s *Sink) Hide(keys ...string) *Sink { 274 | if atomic.LoadInt32(s.state) > sinkClosed { 275 | s.Lock() 276 | for _, key := range keys { 277 | s.hiddenKeys[key] = true 278 | } 279 | s.Unlock() 280 | } 281 | return s 282 | } 283 | 284 | // Unhide previously hidden keys. They will be displayed in the output again. 285 | func (s *Sink) Unhide(keys ...string) *Sink { 286 | if atomic.LoadInt32(s.state) > sinkClosed { 287 | s.Lock() 288 | for _, key := range keys { 289 | delete(s.hiddenKeys, key) 290 | } 291 | s.Unlock() 292 | } 293 | return s 294 | } 295 | 296 | // Stop stops writing to the output. 297 | func (s *Sink) Stop() *Sink { 298 | atomic.StoreInt32(s.state, sinkStopped) 299 | return s 300 | } 301 | 302 | // Start writing to the output. 303 | // After creation of a new sink it will paused and you need explicitly start it. 304 | // It allows setup the filters before the sink will accepts any records. 305 | func (s *Sink) Start() *Sink { 306 | atomic.StoreInt32(s.state, sinkActive) 307 | return s 308 | } 309 | 310 | // Close closes the sink. It flushes records for the sink before closing. 311 | func (s *Sink) Close() { 312 | if atomic.LoadInt32(s.state) > sinkClosed { 313 | atomic.StoreInt32(s.state, sinkClosed) 314 | s.close <- struct{}{} 315 | collector.Lock() 316 | for i, v := range collector.sinks { 317 | if s == v { 318 | collector.sinks[i] = collector.sinks[len(collector.sinks)-1] 319 | collector.sinks[len(collector.sinks)-1] = nil 320 | collector.sinks = collector.sinks[:len(collector.sinks)-1] 321 | break 322 | } 323 | } 324 | collector.Unlock() 325 | } 326 | } 327 | 328 | func processSink(s *Sink) { 329 | var ( 330 | record chain 331 | ok bool 332 | ) 333 | for { 334 | select { 335 | case record, ok = <-s.In: 336 | if !ok { 337 | return 338 | } 339 | if atomic.LoadInt32(s.state) < sinkActive { 340 | record.wg.Done() 341 | continue 342 | } 343 | s.RLock() 344 | var filter Filter 345 | for _, pair := range record.pairs { 346 | // Negative conditions have highest priority 347 | if filter, ok = s.negativeFilters[pair.Key]; ok { 348 | if filter.Check(pair.Key, pair.Val) { 349 | goto skipRecord 350 | } 351 | } 352 | // At last check for positive conditions 353 | if filter, ok = s.positiveFilters[pair.Key]; ok { 354 | if !filter.Check(pair.Key, pair.Val) { 355 | goto skipRecord 356 | } 357 | } 358 | } 359 | s.formatRecord(record.pairs) 360 | skipRecord: 361 | s.RUnlock() 362 | record.wg.Done() 363 | case <-s.close: 364 | s.Lock() 365 | s.positiveFilters = nil 366 | s.negativeFilters = nil 367 | s.hiddenKeys = nil 368 | s.Unlock() 369 | return 370 | } 371 | } 372 | } 373 | 374 | func (s *Sink) formatRecord(record []*Pair) { 375 | s.format.Begin() 376 | for _, pair := range record { 377 | if ok := s.hiddenKeys[pair.Key]; ok { 378 | continue 379 | } 380 | s.format.Pair(pair.Key, pair.Val, pair.Type) 381 | } 382 | s.writer.Write(s.format.Finish()) 383 | } 384 | 385 | const flushTimeout = 3 * time.Second 386 | 387 | func sinkRecord(rec []*Pair) { 388 | var wg sync.WaitGroup 389 | collector.RLock() 390 | for _, s := range collector.sinks { 391 | if atomic.LoadInt32(s.state) == sinkActive { 392 | wg.Add(1) 393 | s.In <- chain{&wg, rec} 394 | } 395 | } 396 | collector.RUnlock() 397 | var c = make(chan struct{}) 398 | go func() { 399 | defer close(c) 400 | wg.Wait() 401 | }() 402 | select { 403 | case <-c: 404 | return 405 | case <-time.After(flushTimeout): 406 | return 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /sink_test.go: -------------------------------------------------------------------------------- 1 | package kiwi 2 | 3 | /* 4 | Copyright (c) 2016, Alexander I.Grafov 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of kvlog nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ॐ तारे तुत्तारे तुरे स्व 33 | 34 | All tests consists of three parts: 35 | 36 | - arrange structures and initialize objects for use in tests 37 | - act on testing object 38 | - check and assert on results 39 | 40 | These parts separated by empty lines in each test function. 41 | */ 42 | 43 | import ( 44 | "bytes" 45 | "strings" 46 | "testing" 47 | "time" 48 | ) 49 | 50 | // Test of log to the stopped sink. 51 | func TestSink_LogToStoppedSink_Logfmt(t *testing.T) { 52 | stream := bytes.NewBufferString("") 53 | log := New() 54 | out := SinkTo(stream, AsLogfmt()) 55 | 56 | log.Log("key", "The sample string that should be ignored.") 57 | 58 | out.Close() 59 | if strings.TrimSpace(stream.String()) != "" { 60 | println(stream.String()) 61 | t.Fail() 62 | } 63 | } 64 | 65 | // Test of log to the stopped sink. 66 | func TestSink_LogToStoppedSink_JSON(t *testing.T) { 67 | stream := bytes.NewBufferString("") 68 | log := New() 69 | out := SinkTo(stream, AsJSON()) 70 | 71 | log.Log("key", "The sample string that should be ignored.") 72 | 73 | out.Close() 74 | if strings.TrimSpace(stream.String()) != "" { 75 | println(stream.String()) 76 | t.Fail() 77 | } 78 | } 79 | 80 | // Test of log to the stopped sink. It should not crash logger. 81 | func TestSink_StopTwice(t *testing.T) { 82 | out := SinkTo(bytes.NewBufferString(""), AsLogfmt()) 83 | out.Stop() 84 | out.Close() 85 | } 86 | 87 | // Test of start already started sink. It should not crash logger. 88 | func TestSink_StartTwice(t *testing.T) { 89 | out := SinkTo(bytes.NewBufferString(""), AsLogfmt()).Start() 90 | out.Start() 91 | out.Close() 92 | } 93 | 94 | // Test of the close already closed sink. It should not crash logger. 95 | func TestSink_CloseTwice(t *testing.T) { 96 | out := SinkTo(bytes.NewBufferString(""), AsLogfmt()) 97 | out.Close() 98 | out.Close() 99 | } 100 | 101 | // Test of reuse of the already created sink. 102 | func TestSink_SinkReuse(t *testing.T) { 103 | stream := bytes.NewBufferString("") 104 | out := SinkTo(stream, AsLogfmt()) 105 | 106 | SinkTo(stream, AsJSON()) 107 | SinkTo(stream, AsLogfmt()) 108 | 109 | out.Close() 110 | } 111 | 112 | // Test of HideKey. It should pass record to the output. 113 | func TestSink_HideKey(t *testing.T) { 114 | stream := bytes.NewBufferString("") 115 | log := New() 116 | out := SinkTo(stream, AsLogfmt()) 117 | 118 | out.Start().Hide("two") 119 | log.Log("one", 1, "two", 2, "three", 3) 120 | 121 | out.Close() 122 | if strings.TrimSpace(stream.String()) != `one=1 three=3` { 123 | println(stream.String()) 124 | t.Fail() 125 | } 126 | } 127 | 128 | // Test of UnhideKey. It should pass record to the output. 129 | func TestSink_UnhideKey(t *testing.T) { 130 | stream := bytes.NewBufferString("") 131 | log := New() 132 | out := SinkTo(stream, AsLogfmt()) 133 | 134 | out.Hide("two").Start().Unhide("two") 135 | log.Log("one", 1, "two", 2, "three", 3) 136 | 137 | out.Close() 138 | if strings.TrimSpace(stream.String()) != `one=1 two=2 three=3` { 139 | println(stream.String()) 140 | t.Fail() 141 | } 142 | } 143 | 144 | // Test of unhide already unhidden key. It should pass record to the output. 145 | func TestSink_UnhideKeyTwice(t *testing.T) { 146 | stream := bytes.NewBufferString("") 147 | log := New() 148 | out := SinkTo(stream, AsLogfmt()) 149 | 150 | out.Start().Unhide("one").Unhide("one") 151 | log.Log("one", 1, "two", 2) 152 | 153 | out.Close() 154 | if strings.TrimSpace(stream.String()) != `one=1 two=2` { 155 | println(stream.String()) 156 | t.Fail() 157 | } 158 | 159 | } 160 | 161 | // Test of HasKey filter. It should pass record to the output. 162 | func TestSink_HasKeyFilterPass(t *testing.T) { 163 | stream := bytes.NewBufferString("") 164 | log := New() 165 | out := SinkTo(stream, AsLogfmt()).HasKey("Gandalf").Start() 166 | 167 | log.Log("Gandalf", "You shall not pass!") // cite from the movie 168 | 169 | out.Close() 170 | if strings.TrimSpace(stream.String()) != `Gandalf="You shall not pass!"` { 171 | println(stream.String()) 172 | t.Fail() 173 | } 174 | 175 | } 176 | 177 | // Test of HasNotKey filter. It should not pass record to the output. 178 | func TestSink_HasNotKeyFilterOut(t *testing.T) { 179 | stream := bytes.NewBufferString("") 180 | log := New() 181 | out := SinkTo(stream, AsLogfmt()).HasNotKey("Gandalf").Start() 182 | 183 | log.Log("Gandalf", "You cannot pass!") // cite from the book 184 | 185 | out.Close() 186 | if strings.TrimSpace(stream.String()) != "" { 187 | println(stream.String()) 188 | t.Fail() 189 | } 190 | } 191 | 192 | // Test of HasValue filter. It should pass the record to the output because the key missed. 193 | func TestSink_HasValueFilterMissedKeyPass(t *testing.T) { 194 | stream := bytes.NewBufferString("") 195 | log := New() 196 | out := SinkTo(stream, AsLogfmt()).HasValue("key", "passed").Start() 197 | 198 | log.Log("key", "passed") 199 | 200 | out.Close() 201 | if strings.TrimSpace(stream.String()) != `key="passed"` { 202 | println(stream.String()) 203 | t.Fail() 204 | } 205 | 206 | } 207 | 208 | // Test of HasValue filter. It should pass the record to the output because the value matched. 209 | func TestSink_HasValueFilterPass(t *testing.T) { 210 | stream := bytes.NewBufferString("") 211 | log := New() 212 | out := SinkTo(stream, AsLogfmt()).HasValue("key", "passed", "and this passed too").Start() 213 | 214 | log.Log("key", "passed", "key", "and this passed too") 215 | 216 | out.Close() 217 | if strings.TrimSpace(stream.String()) != `key="passed" key="and this passed too"` { 218 | println(stream.String()) 219 | t.Fail() 220 | } 221 | } 222 | 223 | // Test of HasValue filter. It should filter out the record because no one value matched. 224 | func TestSink_HasValueFilterOut(t *testing.T) { 225 | stream := bytes.NewBufferString("") 226 | log := New() 227 | out := SinkTo(stream, AsLogfmt()).HasValue("key", "filtered", "out").Start() 228 | 229 | log.Log("key", "try it") 230 | 231 | out.Close() 232 | if strings.TrimSpace(stream.String()) != "" { 233 | println(stream.String()) 234 | t.Fail() 235 | } 236 | } 237 | 238 | // Test of HasIntRange filter. It should pass the record to the output because the key missed. 239 | func TestSink_HasIntRangeFilterMissedKeyPass(t *testing.T) { 240 | stream := bytes.NewBufferString("") 241 | log := New() 242 | out := SinkTo(stream, AsLogfmt()).Int64Range("key", 1, 2).Start() 243 | 244 | log.Log("another key", 3) 245 | 246 | out.Close() 247 | if strings.TrimSpace(stream.String()) != `"another key"=3` { 248 | println(stream.String()) 249 | t.Fail() 250 | } 251 | } 252 | 253 | // Test of IntRange filter. It should pass the record to the output because the value in the range. 254 | func TestSink_IntRangeFilterPass(t *testing.T) { 255 | stream := bytes.NewBufferString("") 256 | log := New() 257 | out := SinkTo(stream, AsLogfmt()).Int64Range("key", 1, 3).Start() 258 | 259 | log.Log("key", 2) 260 | 261 | out.Close() 262 | if strings.TrimSpace(stream.String()) != `key=2` { 263 | println(stream.String()) 264 | t.Fail() 265 | } 266 | } 267 | 268 | // Test of IntRange filter. It should filter out the record because the value not in the range. 269 | func TestSink_IntRangeFilterFilterOut(t *testing.T) { 270 | stream := bytes.NewBufferString("") 271 | log := New() 272 | out := SinkTo(stream, AsLogfmt()).Int64Range("key", 1, 3).Start() 273 | 274 | log.Log("key", 4) 275 | 276 | out.Close() 277 | if strings.TrimSpace(stream.String()) != "" { 278 | println(stream.String()) 279 | t.Fail() 280 | } 281 | } 282 | 283 | // Test of FloatRange filter. It should pass the record to the output because the key missed. 284 | func TestSink_FloatRangeFilterMissedKeyPass(t *testing.T) { 285 | stream := bytes.NewBufferString("") 286 | log := New() 287 | out := SinkTo(stream, AsLogfmt()).Float64Range("key", 1.0, 2.0).Start() 288 | 289 | log.Log("another key", 3) 290 | 291 | out.Close() 292 | if strings.TrimSpace(stream.String()) != `"another key"=3` { 293 | println(stream.String()) 294 | t.Fail() 295 | } 296 | } 297 | 298 | // Test of FloatRange filter. It should pass the record to the output because the value in the range. 299 | func TestSink_FloatRangeFilterPass(t *testing.T) { 300 | stream := bytes.NewBufferString("") 301 | log := New() 302 | out := SinkTo(stream, AsLogfmt()).Float64Range("key", 1.0, 3.0).Start() 303 | 304 | log.Log("key", 2.0) 305 | 306 | out.Close() 307 | if strings.TrimSpace(stream.String()) != `key=2e+00` { 308 | println(stream.String()) 309 | t.Fail() 310 | } 311 | } 312 | 313 | // Test of FloatRange filter. It should filter out the record because the value not in the range. 314 | func TestSink_FloatRangeFilterOut(t *testing.T) { 315 | stream := bytes.NewBufferString("") 316 | log := New() 317 | out := SinkTo(stream, AsLogfmt()).Float64Range("key", 1.0, 3.0).Start() 318 | 319 | log.Log("key", 4.0) 320 | 321 | out.Close() 322 | if strings.TrimSpace(stream.String()) != "" { 323 | println(stream.String()) 324 | t.Fail() 325 | } 326 | } 327 | 328 | // Test of TimeRange filter. It should pass the record to the output because the value in the range. 329 | func TestSink_TimeRangeFilterPass(t *testing.T) { 330 | stream := bytes.NewBufferString("") 331 | log := New() 332 | now := time.Now() 333 | hourAfterNow := now.Add(1 * time.Hour) 334 | halfHourAfterNow := now.Add(30 * time.Minute) 335 | halfHourAsString := halfHourAfterNow.Format(TimeLayout) 336 | out := SinkTo(stream, AsLogfmt()).TimeRange("key", now, hourAfterNow).Start() 337 | 338 | log.Log("key", halfHourAfterNow) 339 | 340 | out.Close() 341 | if strings.TrimSpace(stream.String()) != `key=`+halfHourAsString { 342 | println(stream.String()) 343 | t.Fail() 344 | } 345 | } 346 | 347 | // Test of WithTimeRange filter. It should filter out the record because the value not in the range. 348 | func TestSink_TimeRangeFilterOut(t *testing.T) { 349 | stream := bytes.NewBufferString("") 350 | log := New() 351 | now := time.Now() 352 | hourAfterNow := now.Add(1 * time.Hour) 353 | halfHourAfterNow := now.Add(30 * time.Minute) 354 | out := SinkTo(stream, AsLogfmt()).TimeRange("key", now, halfHourAfterNow).Start() 355 | 356 | log.Log("key", hourAfterNow) 357 | 358 | out.Close() 359 | if strings.TrimSpace(stream.String()) != "" { 360 | println(stream.String()) 361 | t.Fail() 362 | } 363 | } 364 | 365 | type customFilterThatReturnsTrue struct{} 366 | 367 | func (customFilterThatReturnsTrue) Check(key, val string) bool { 368 | return true 369 | } 370 | 371 | // Test of WithFilter custom filter. It should pass the record to the output because the value in the range. 372 | func TestSink_WithCustomPass(t *testing.T) { 373 | stream := bytes.NewBufferString("") 374 | log := New() 375 | var customFilter customFilterThatReturnsTrue 376 | out := SinkTo(stream, AsLogfmt()).WithFilter("key", customFilter).Start() 377 | 378 | log.Log("key", 2) 379 | 380 | out.Close() 381 | if strings.TrimSpace(stream.String()) != `key=2` { 382 | println(stream.String()) 383 | t.Fail() 384 | } 385 | } 386 | 387 | // Test of WithFilter custom filter. It should pass the record to the output because the key missed. 388 | func TestSink_WithCustomMissedKeyPass(t *testing.T) { 389 | stream := bytes.NewBufferString("") 390 | log := New() 391 | var customFilter customFilterThatReturnsTrue 392 | out := SinkTo(stream, AsLogfmt()).WithFilter("key", customFilter).Start() 393 | 394 | log.Log("another key", 3) 395 | 396 | out.Close() 397 | if strings.TrimSpace(stream.String()) != `"another key"=3` { 398 | println(stream.String()) 399 | t.Fail() 400 | } 401 | } 402 | 403 | type customFilterThatReturnsFalse struct{} 404 | 405 | func (customFilterThatReturnsFalse) Check(key, val string) bool { 406 | return false 407 | } 408 | 409 | // Test of WithFilter custom filter. It should pass the record to the output because the value in the range. 410 | func TestSink_WithCustomFilterOut(t *testing.T) { 411 | stream := bytes.NewBufferString("") 412 | log := New() 413 | var customFilter customFilterThatReturnsFalse 414 | out := SinkTo(stream, AsLogfmt()).WithFilter("key", customFilter).Start() 415 | 416 | log.Log("key", 2) 417 | 418 | out.Close() 419 | if strings.TrimSpace(stream.String()) != "" { 420 | println(stream.String()) 421 | t.Fail() 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /global-logger_test.go: -------------------------------------------------------------------------------- 1 | package kiwi_test 2 | 3 | /* 4 | Copyright (c) 2016-2019, Alexander I.Grafov 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of kvlog nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ॐ तारे तुत्तारे तुरे स्व 33 | */ 34 | 35 | import ( 36 | "github.com/grafov/kiwi" 37 | 38 | "bytes" 39 | "fmt" 40 | "strings" 41 | "testing" 42 | "time" 43 | ) 44 | 45 | // Test logging of string value. 46 | func TestGlobalLogger_LogSingleValue_Logfmt(t *testing.T) { 47 | output := bytes.NewBufferString("") 48 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 49 | 50 | kiwi.Log("single value without key") 51 | 52 | out.Close() 53 | if strings.TrimSpace(output.String()) != `message="single value without key"` { 54 | println(output.String()) 55 | t.Fail() 56 | } 57 | } 58 | 59 | // Test logging of string value. 60 | func TestGlobalLogger_LogStringValue_Logfmt(t *testing.T) { 61 | output := bytes.NewBufferString("") 62 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 63 | 64 | kiwi.Log("k", "The sample string with a lot of spaces.") 65 | 66 | out.Close() 67 | if strings.TrimSpace(output.String()) != "k=\"The sample string with a lot of spaces.\"" { 68 | println(output.String()) 69 | t.Fail() 70 | } 71 | } 72 | 73 | // Test logging of byte array. 74 | func TestGlobalLogger_LogBytesValue_Logfmt(t *testing.T) { 75 | output := bytes.NewBufferString("") 76 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 77 | 78 | kiwi.Log("k", []byte("The sample string with a lot of spaces.")) 79 | 80 | out.Close() 81 | if strings.TrimSpace(output.String()) != "k=\"The sample string with a lot of spaces.\"" { 82 | println(output.String()) 83 | t.Fail() 84 | } 85 | } 86 | 87 | // Test logging of integer value. 88 | func TestGlobalLogger_LogIntValue_Logfmt(t *testing.T) { 89 | output := bytes.NewBufferString("") 90 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 91 | 92 | kiwi.Log("k", 123) 93 | 94 | out.Close() 95 | if strings.TrimSpace(output.String()) != "k=123" { 96 | println(output.String()) 97 | t.Fail() 98 | } 99 | } 100 | 101 | // Test logging of negative integer value. 102 | func TestGlobalLogger_LogNegativeIntValue_Logfmt(t *testing.T) { 103 | output := bytes.NewBufferString("") 104 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 105 | 106 | kiwi.Log("k", 123) 107 | 108 | out.Close() 109 | if strings.TrimSpace(output.String()) != "k=123" { 110 | println(output.String()) 111 | t.Fail() 112 | } 113 | } 114 | 115 | // Test logging of float value in default (scientific) format. 116 | func TestGlobalLogger_LogFloatValue_Logfmt(t *testing.T) { 117 | output := bytes.NewBufferString("") 118 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 119 | 120 | kiwi.Log("k", 3.14159265359) 121 | 122 | out.Close() 123 | if strings.TrimSpace(output.String()) != "k=3.14159265359e+00" { 124 | println(output.String()) 125 | t.Fail() 126 | } 127 | } 128 | 129 | // Test logging of float value in fixed format. 130 | func TestGlobalLogger_LogFixedFloatValue_Logfmt(t *testing.T) { 131 | output := bytes.NewBufferString("") 132 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 133 | 134 | kiwi.FloatFormat = 'f' 135 | kiwi.Log("k", 3.14159265359) 136 | 137 | out.Close() 138 | if strings.TrimSpace(output.String()) != "k=3.14159265359" { 139 | println(output.String()) 140 | t.Fail() 141 | } 142 | // Turn back to default format. 143 | kiwi.FloatFormat = 'e' 144 | } 145 | 146 | // Test logging of boolean value. 147 | func TestGlobalLogger_LogBoolValue_Logfmt(t *testing.T) { 148 | output := bytes.NewBufferString("") 149 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 150 | 151 | kiwi.Log("k", true, "k2", false) 152 | 153 | out.Close() 154 | if strings.TrimSpace(output.String()) != "k=true k2=false" { 155 | println(output.String()) 156 | t.Fail() 157 | } 158 | } 159 | 160 | // Test logging of complex number. 161 | func TestGlobalLogger_LogComplexValue_Logfmt(t *testing.T) { 162 | output := bytes.NewBufferString("") 163 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 164 | 165 | kiwi.Log("k", .12345e+5i, "k2", 1.e+0i) 166 | 167 | out.Close() 168 | if strings.TrimSpace(output.String()) != "k=(0.000000+12345.000000i) k2=(0.000000+1.000000i)" { 169 | println(output.String()) 170 | t.Fail() 171 | } 172 | } 173 | 174 | // Test logging of time literal. 175 | func TestGlobalLogger_LogTimeValue_Logfmt(t *testing.T) { 176 | output := bytes.NewBufferString("") 177 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 178 | value := time.Now() 179 | valueString := value.Format(kiwi.TimeLayout) 180 | 181 | kiwi.Log("k", value) 182 | 183 | out.Close() 184 | if strings.TrimSpace(output.String()) != fmt.Sprintf("k=%s", valueString) { 185 | println(output.String()) 186 | t.Fail() 187 | } 188 | } 189 | 190 | // Test logging of the numeric key. 191 | func TestGlobalLogger_LogNumericKeyAsString_Logfmt(t *testing.T) { 192 | output := bytes.NewBufferString("") 193 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 194 | 195 | kiwi.Log("123", "The sample value.") 196 | 197 | out.Close() 198 | if strings.TrimSpace(output.String()) != "123=\"The sample value.\"" { 199 | println(output.String()) 200 | t.Fail() 201 | } 202 | } 203 | 204 | // The logger accepts string keys. In other cases it complains about 205 | // the wrong key type. It also assumes that each key followed by the 206 | // value. 207 | func TestGlobalLogger_LogNumericKeyNotValid_Logfmt(t *testing.T) { 208 | output := bytes.NewBufferString("") 209 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 210 | 211 | kiwi.Log(123, "The sample value.") 212 | 213 | out.Close() 214 | expected := `kiwi-error="non a string type (int) for the key (123)" message="The sample value."` 215 | if strings.TrimSpace(output.String()) != expected { 216 | t.Logf("expected %s got %v", expected, output.String()) 217 | t.Fail() 218 | } 219 | } 220 | 221 | // The logger accepts string keys. In other cases it complains about 222 | // the wrong key type. It also assumes that each key followed by the 223 | // value. 224 | func TestGlobalLogger_LogTwoNumericsNotValid_Logfmt(t *testing.T) { 225 | output := bytes.NewBufferString("") 226 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 227 | 228 | kiwi.Log(12, 34) 229 | 230 | out.Close() 231 | expected := `kiwi-error="non a string type (int) for the key (12)" message=34` 232 | if strings.TrimSpace(output.String()) != expected { 233 | t.Logf("expected %s got %v", expected, output.String()) 234 | t.Fail() 235 | } 236 | } 237 | 238 | // The logger accepts string keys. In other cases it complains about 239 | // the wrong key type. It also assumes that each key followed by the 240 | // value. 241 | func TestGlobalLogger_LogThreeNumericsNotValid_Logfmt(t *testing.T) { 242 | output := bytes.NewBufferString("") 243 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 244 | 245 | kiwi.Log(12, 34, 56) 246 | 247 | out.Close() 248 | expected := `kiwi-error="non a string type (int) for the key (12)" message=34 kiwi-error="non a string type (int) for the key (56)"` 249 | if strings.TrimSpace(output.String()) != expected { 250 | t.Logf("expected %s got %v", expected, output.String()) 251 | t.Fail() 252 | } 253 | } 254 | 255 | // The logger accepts string keys. In other cases it complains about 256 | // the wrong key type. It also assumes that each key followed by the 257 | // value. 258 | func TestGlobalLogger_LogFourNumericsNotValid_Logfmt(t *testing.T) { 259 | output := bytes.NewBufferString("") 260 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 261 | 262 | kiwi.Log(12, 34, 56, 78) 263 | 264 | out.Close() 265 | expected := `kiwi-error="non a string type (int) for the key (12)" message=34 kiwi-error="non a string type (int) for the key (56)" message=78` 266 | if strings.TrimSpace(output.String()) != expected { 267 | t.Logf("expected %s got %v", expected, output.String()) 268 | t.Fail() 269 | } 270 | } 271 | 272 | // Test logging of the key with spaces. 273 | func TestGlobalLogger_LogKeyWithSpaces_Logfmt(t *testing.T) { 274 | output := bytes.NewBufferString("") 275 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 276 | 277 | kiwi.Log("key with spaces", "The sample value.") 278 | 279 | out.Close() 280 | if strings.TrimSpace(output.String()) != "\"key with spaces\"=\"The sample value.\"" { 281 | println(output.String()) 282 | t.Fail() 283 | } 284 | } 285 | 286 | // Test logging of the key with tabs. 287 | func TestGlobalLogger_LogKeyWithTabs_Logfmt(t *testing.T) { 288 | output := bytes.NewBufferString("") 289 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 290 | 291 | kiwi.Log(fmt.Sprintf("key\twith\ttabs"), "The sample value.") 292 | 293 | out.Close() 294 | if strings.TrimSpace(output.String()) != "\"key\\twith\\ttabs\"=\"The sample value.\"" { 295 | println(output.String()) 296 | t.Fail() 297 | } 298 | } 299 | 300 | // Test logging of the multi lines key. 301 | func TestGlobalLogger_LogKeyMultiLine_Logfmt(t *testing.T) { 302 | output := bytes.NewBufferString("") 303 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 304 | 305 | kiwi.Log(fmt.Sprintf("multi\nlines\nkey"), "The sample value.") 306 | 307 | out.Close() 308 | if strings.TrimSpace(output.String()) != "\"multi\\nlines\\nkey\"=\"The sample value.\"" { 309 | println(output.String()) 310 | t.Fail() 311 | } 312 | } 313 | 314 | // Test logging of the multi lines value. 315 | func TestGlobalLogger_LogValueMultiLine_Logfmt(t *testing.T) { 316 | output := bytes.NewBufferString("") 317 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 318 | 319 | kiwi.Log("key", fmt.Sprintf("multi\nlines\nvalue")) 320 | 321 | out.Close() 322 | if strings.TrimSpace(output.String()) != "key=\"multi\\nlines\\nvalue\"" { 323 | println(output.String()) 324 | t.Fail() 325 | } 326 | } 327 | 328 | // Test log with the context value. 329 | func TestGlobalLogger_WithContextPassed_Logfmt(t *testing.T) { 330 | kiwi.ResetContext() 331 | output := bytes.NewBufferString("") 332 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 333 | 334 | kiwi.With("key1", "value") 335 | kiwi.Log("key2", "value") 336 | 337 | out.Close() 338 | if strings.TrimSpace(output.String()) != `key1="value" key2="value"` { 339 | t.Fail() 340 | } 341 | } 342 | 343 | // The context accepts even number of keys and values for merging them 344 | // into pairs. It the case with the odd number of values the last 345 | // value prepended with default key "message". 346 | func TestGlobalLogger_WithOddContextPassed_Logfmt(t *testing.T) { 347 | kiwi.ResetContext() 348 | output := bytes.NewBufferString("") 349 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 350 | 351 | kiwi.With("key1", "value", "orphan-value") 352 | kiwi.Log("key2", "value") 353 | 354 | out.Close() 355 | got := output.String() 356 | expected := `key1="value"` 357 | if !strings.Contains(got, expected) { 358 | t.Logf("expected %s got %v", expected, got) 359 | t.Fail() 360 | } 361 | expected = `message="orphan-value"` 362 | if !strings.Contains(got, expected) { 363 | t.Logf("expected %s got %v", expected, got) 364 | t.Fail() 365 | } 366 | expected = `key2="value"` 367 | if !strings.Contains(got, expected) { 368 | t.Logf("expected %s got %v", expected, got) 369 | t.Fail() 370 | } 371 | } 372 | 373 | // The context accepts even number of keys and values for merging them 374 | // into pairs. It the case with the odd number of values the last 375 | // value prepended with default key "message". 376 | func TestGlobalLogger_WithKeyOnlyPassed_Logfmt(t *testing.T) { 377 | kiwi.ResetContext() 378 | output := bytes.NewBufferString("") 379 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 380 | 381 | kiwi.With("orphan-value") 382 | kiwi.Log("key2", "value") 383 | 384 | out.Close() 385 | expected := `message="orphan-value" key2="value"` 386 | if strings.TrimSpace(output.String()) != expected { 387 | t.Logf("expected %s got %v", expected, output.String()) 388 | t.Fail() 389 | } 390 | } 391 | 392 | // The logger and context accept even number of keys and values for 393 | // merging them into pairs. It the case with the odd number of values 394 | // the last value prepended with default key "message". 395 | func TestGlobalLogger_OddArgsInvalid_Logfmt(t *testing.T) { 396 | kiwi.ResetContext() 397 | output := bytes.NewBufferString("") 398 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 399 | 400 | kiwi.With("orphan-context") 401 | kiwi.Log("orphan-value") 402 | 403 | out.Close() 404 | expected := `message="orphan-context" message="orphan-value"` 405 | if strings.TrimSpace(output.String()) != expected { 406 | t.Logf("expected %s got %v", expected, output.String()) 407 | t.Fail() 408 | } 409 | } 410 | 411 | // The context accepts string keys. In other cases it complains about 412 | // the wrong key type. It also assumes that each key followed by the 413 | // value. 414 | func TestGlobalLogger_WithIntKeyInvalid_Logfmt(t *testing.T) { 415 | kiwi.ResetContext() 416 | output := bytes.NewBufferString("") 417 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 418 | 419 | kiwi.With(123, 456) 420 | kiwi.Log("key2", "value") 421 | 422 | out.Close() 423 | got := output.String() 424 | expected := `kiwi-error="wrong type for the key"` 425 | if !strings.Contains(got, expected) { 426 | t.Logf("expected %s got %v", expected, got) 427 | t.Fail() 428 | } 429 | expected = `message=456` 430 | if !strings.Contains(got, expected) { 431 | t.Logf("expected %s got %v", expected, got) 432 | t.Fail() 433 | } 434 | expected = `key2="value"` 435 | if !strings.Contains(got, expected) { 436 | t.Logf("expected %s got %v", expected, got) 437 | t.Fail() 438 | } 439 | } 440 | 441 | // Test log with adding then removing the context. 442 | func TestGlobalLogger_WithoutContextPassed_Logfmt(t *testing.T) { 443 | kiwi.ResetContext() 444 | output := bytes.NewBufferString("") 445 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 446 | 447 | // add the context 448 | kiwi.With("key1", "value") 449 | // remove the context 450 | kiwi.Without("key1") 451 | // add regular pair 452 | kiwi.Log("key2", "value") 453 | 454 | out.Flush() 455 | if strings.TrimSpace(output.String()) != `key2="value"` { 456 | t.Fail() 457 | } 458 | } 459 | 460 | // Test log with adding then reset the context. 461 | func TestGlobalLogger_ResetContext_Logfmt(t *testing.T) { 462 | kiwi.ResetContext() 463 | output := bytes.NewBufferString("") 464 | out := kiwi.SinkTo(output, kiwi.AsLogfmt()).Start() 465 | 466 | // add the context 467 | kiwi.With("key1", "value") 468 | // reset the context 469 | kiwi.ResetContext() 470 | // add regular pair 471 | kiwi.Log("key2", "value") 472 | 473 | out.Flush() 474 | if strings.TrimSpace(output.String()) != `key2="value"` { 475 | t.Fail() 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Kiwi logger & context keeper [![Go Report Card](https://goreportcard.com/badge/grafov/kiwi)](https://goreportcard.com/report/grafov/kiwi) [![Coverage Status](https://coveralls.io/repos/github/grafov/kiwi/badge.svg?branch=master)](https://coveralls.io/github/grafov/kiwi?branch=master) 3 | 4 | [![DeepSource](https://static.deepsource.io/deepsource-badge-light.svg)](https://deepsource.io/gh/grafov/kiwi/?ref=repository-badge) 5 | 6 | *The project is long live experiment. Use it carefully.* 7 | 8 | ![Kiwi bird](flomar-kiwi-bird-300px.png) 9 | 10 | *Kiwi* /kiːwiː/ are birds native to New Zealand, in the genus Apteryx and family Apterygidae. They 11 | are flightless, have hair-like feathers and smell like mushrooms. They look strange and funny so 12 | when I wrote a logger for Go language I decided to devote it to this beast which I never seen in a 13 | wild (I live very far from places where kiwis are live). 14 | 15 | *Kiwi Logger* — this is a library with an odd logic that log your application data in its own strange way. 16 | 17 | Well... `kiwi` package is structured logger for key-value formats (with *logfmt* as default), it 18 | highly customizable without hardcoded levels or nailed fields. It has dynamic filters that allow you 19 | change you logs on the fly in two aspects: reduce number of records and restrict fields for each 20 | record. It allows fine control on that should be logged in the moment and especially useful for 21 | debugging. So `kiwi` separates output logic (when and where you want write) from logging process 22 | itself (just log anything what you want). 23 | 24 | ## Features offered by structered logging and logfmt generally and by Kiwi particularly 25 | 26 | * simple logfmt-like format for high readability by humans 27 | * JSON format that liked by robots 28 | * no hardcoded levels 29 | * change log verbosity and set of record fields on the fly 30 | * dynamic filtering 31 | * keep context of the application 32 | * fast forking of subloggers with inherited context 33 | * optional lazy evaluation of arguments for lowering logger footprint 34 | 35 | Kiwi logger has built around the idea: 36 | 37 | *Log everything in the code with as much details as possible. But actually write only that you need in the moment.* 38 | 39 | In the ideal world of course you could log _everything_ and it is right way. Then you are look up 40 | the central log storage that gathered logs from all the applications instances. You could view and 41 | filter the records you are interested in the moment. But in the reality the most of the systems 42 | developed locally and often you have no central log storage in development environment. And the 43 | `grep` utility is only the interface for logs filtering. 44 | 45 | For example you are in debugging of the `feature1` that spread across several modules of your 46 | system. You are instrumenting the code with multiple `log()` calls and set verbosity level to 47 | `DEBUG`. Then you have done with the feature and decrease you level to `ERROR`. Then you are begin 48 | to debug `feature2` that spread even more across the modules of your system. And all repeat 49 | again. You are set the level to `DEBUG` and you are see the records both for the `feature2` you are 50 | need and for `feature1` from the past. Welcome to grepping. 51 | 52 | The "logfmt" format solves this problem with tags. The log records consists of arbitrary number of 53 | key-value pairs. You can easily filter the only records you are need by the keys. The kiwi logger 54 | allows you set filters dynamically in runtime. 55 | 56 | ## Architecture? 57 | 58 | ![Kiwi vs other loggers](kiwi-vs-other.png) 59 | 60 | Scared? ;) Ha-ha... Well, really it is not too weird as this picture looks :) Let me explain with more 61 | clear and boring illustrations. 62 | 63 | ![Kiwi flow](flow.png) 64 | 65 | The logger instances (in different goroutines for example) write everything what you want to 66 | log. _Sinks_ gather the data from all the instances. _Sink_ is the name for the output — it could be 67 | a file or stdout or any other thing that realizes `io.Writer()`. Filters for the sinks have rules to 68 | pass only records you are really want for this output. For example you can dedicate the file for 69 | errors come from `module1` and another file for errors and warnings that come from `module2`. 70 | 71 | For example you can pass details of the record to a logfile for full debug. But write only 72 | important information with an error message and status to stderr. 73 | 74 | Recipe: export the handler or setup any kind of client for setting these filters in your app. Then 75 | you get ability for dynamically change the flow and the verbosity of logs. For example increase 76 | verbosity for a specific module or a single handler and decrease them for the rest of the 77 | application. 78 | 79 | ## Docs [![GoDoc](https://godoc.org/github.com/grafov/kiwi?status.svg)](https://godoc.org/github.com/grafov/kiwi) 80 | 81 | See documentation in [the wiki](https://github.com/grafov/kiwi/wiki). Examples of logger usage see 82 | at [cmd/*](cmd) subfolders. And of course for API description look at 83 | [godoc](http://godoc.org/github.com/grafov/kiwi). 84 | 85 | ## Installation [![Build Status](https://travis-ci.org/grafov/kiwi.svg?branch=master)](https://travis-ci.org/grafov/kiwi) 86 | 87 | Package have no external dependencies except standard library. So just 88 | 89 | go get github.com/grafov/kiwi 90 | 91 | The library builds has been tested with go 1.8. 92 | 93 | ## Usage examples 94 | 95 | ```go 96 | import "github.com/grafov/kiwi" 97 | 98 | func main() { 99 | // Creates a new logger instance. 100 | log:=kiwi.New() 101 | 102 | // Now just log something as key/value pair. It will pass to output immediately (read about outputs below). 103 | log.Log("msg", "something", "another key", "another value") 104 | // Expected output: 105 | // msg="something" another\ key="another value" 106 | 107 | // You can pass odd number of parameters. Odd parameter passed to output just as is. 108 | log.Log("key-only") 109 | // Expected output: 110 | // "key-only" 111 | 112 | // It can add key=value pairs to a new log record. 113 | // They don't passed to the output until Log() call. 114 | log.Add("where", "module1", "event", "something happened") 115 | 116 | // So it may be any number of Add() calls with additional pairs. 117 | // Then flush them all. 118 | log.Add("event", "and now something completely different").Log() 119 | 120 | // You can pass any scalar types from Go standard library as record keys and values 121 | // they will be converted to their string representation. 122 | log.Log("key", 123, "key2", 1.23e3, "key3", 'u', "key4", true) 123 | // Expected output: 124 | // key=123 key2=1.23e3 key3="u" key4=true 125 | 126 | // You need define even one sink: set writer and logging format. 127 | // Until the sink defined log records just saved nowhere. 128 | // You can define arbitrary number of sinks. Each sink has its own set of filters. 129 | out:=kiwi.SinkTo(os.StdOut, kiwi.Logfmt).Start() 130 | 131 | // Filters decide pass or not incoming log record to this output. 132 | // Example filters below will pass only records which has key "userID" and has value of level="FATAL". 133 | out.WithKey("userID").WithValue("level", "FATAL") 134 | 135 | // So in this manner you can fan out log record to several outputs. 136 | // For example write separate log of critical errors and common log with all errors. 137 | // By default without any filters any output accepts any incoming log records. 138 | out2 := kiwi.SinkTo(os.StdErr, kiwi.JSON).Start() 139 | 140 | // Kiwi offers various filters for set conditions for outputs. 141 | out2.WithInt64Range("userID", 100, 500).WithoutValue("label", "debug") 142 | } 143 | ``` 144 | 145 | See more ready to run samples in `cmd` subdirs. Filters described in the wiki: 146 | [Filtering](https://github.com/grafov/kiwi/wiki/Filtering). 147 | 148 | ## The context records 149 | 150 | `Kiwi` logger allows you keep some pairs during lifetime of a logger instance. 151 | 152 | ```go 153 | import "github.com/grafov/kiwi" 154 | 155 | func main() { 156 | // Creates a new logger instance. 157 | log1 := kiwi.New() 158 | 159 | // You can set permanent pairs as logger context. 160 | log1.With("userID", 1000, "PID", os.GetPID()) 161 | 162 | // They will be passed implicitly amongst other pairs for the each record. 163 | log1.Log("msg", "details about something") 164 | // Expect output: 165 | // userID=1000 PID=12345 msg="details about something" 166 | 167 | // Context copied into a new logger instance after logger cloned. 168 | log2 := log1.New() 169 | 170 | log2.Log("key", "value") 171 | // Expect output: 172 | // userID=1000 PID=12345 key="value" 173 | 174 | // Get previously keeped context values. Results returned as map[string]interface{} 175 | appContext := log2.GetContext() 176 | fmt.Printf("%+v\n", appContext) 177 | 178 | // You can reset context at any time with 179 | log2.ResetContext() 180 | } 181 | ``` 182 | 183 | ## Thread safety 184 | 185 | It is unsafe by design. Firstly I have used version for safe work in multiple goroutines. And it 186 | was not only slow but in just not need in many cases. If you need a new logger in another execution 187 | thread you will create another instanse. Better is clone old instance to a new one for passing the 188 | context to a subroutine. It is all. 189 | 190 | ```go 191 | // Creates a new logger instance. 192 | log1 := kiwi.New().With("context key", "context value") 193 | 194 | // Just clone old instance to a new one. It will keep the context of the first instance. 195 | log2 := log1.New() 196 | 197 | // And you can extend context for cloned instance. 198 | log2.With("another key", "another value") 199 | 200 | // So other concurrent routines may accept logger with the same context. 201 | go subroutine(log2, otherArgs...) 202 | ``` 203 | 204 | For the small apps where you won't init all these instances you would like use global `kiwi.Log()` method. 205 | This method just immediately flush it's args to the sinks. And by design it is safe for concurrent usage. 206 | Also due design simplicity it not supports context, only regular values. If you need context then you 207 | application is complex thing hence you will need initialize a new instance of kiwi.Logger(). 208 | 209 | ## Evaluating rules 210 | 211 | * Keys and values evaluated *immediately* after they added to a record. 212 | * Context values evaluated *once* when they added to a logger. 213 | * For lazy evaluating of context and record values pass them as functions: 214 | 215 | ```go 216 | # For lazy evaluating you need function that returns string 217 | func longActionForDelayedEvaluation() string { 218 | // Do something complex... 219 | // and got for example integer result. 220 | // 221 | // You need convert the result to a string. 222 | return strconv.Itoa(result) 223 | } 224 | myLog.Add("lazy-sample", longActionForDelayedEvaluation) # but not longActionForDelayedEvaluation() 225 | ``` 226 | 227 | Logger accepts functions without args that returns a string: `func () string`. 228 | Hence value of `lazy-sample` from the example above will be evaluated only on `Log()` call. 229 | 230 | ## Additional features in other packages 231 | 232 | I try to keep the main logging package simple. Filters-formatters-sinks concept is core thing in 233 | `kiwi` tag filtering so it all placed in the single package. Alongside with basic formatters for 234 | JSON and Logfmt. All other features I try to move to separate packages. The `kiwi` repository have 235 | the subpackages you could import: 236 | 237 | * [level](level) — imitate traditional syslog-like levels (read more details below) 238 | * [timestamp](timestamp) — provide the logger instance with additional timestamp field 239 | * [strict](strict) — helper functions for providing more type control on your records 240 | 241 | ## Warning about evil severity levels 242 | 243 | The most of loggers came to you with concept of `levels`. So you are filter anything but only 244 | records of the preconigured level and levels above really appear in the log. The current level read 245 | from the configuration of the application. There are loggers that allow you change the level in 246 | runtime. The level here is like the key in logfmt. But logfmt became with more general idea: you can 247 | arbitrary number of keys for the filtering. Not only predefined words for levels but any things like 248 | `feature`, `module` etc. So you can filter not only by the severity but set general taxonomy of the 249 | categories across all the parts (subsystems, modules) of your application. 250 | 251 | Another problem with traditional syslog-like levels is non clear specification what exactly should 252 | pass to each level. Look up the internet for many guides with controversial recommendations how to 253 | distinguish all these "standard" levels and try map them to various events in your application. So 254 | in the reality programmers often debate about level for the log record. For example when you should 255 | use "fatal" instead of "critical" or use "debug" instead of "info". So very often severity levels 256 | obstruct understanding of the logs. 257 | 258 | You can use log syslog-like levels in `kiwi` if you are comfortable with them (see helper functions 259 | in `level` subpackage) and interprete them as you wish. Severity levels in `kiwi` don't play any 260 | role in deciding how to output the record. Any records with any level will pass to all sinks. 261 | Filters in each sink will decide how to actually display the record or filter it out completely. 262 | 263 | ## Instead of FAQ 264 | 265 | 0. Kiwi logger not strictly follows logfmt specs. 266 | 1. Ideas of key-value format are like JSON format but with orientation on readability for humans first. 267 | 2. Yes, it was architectured and developed to be a standard number 15 that competing with others. It is not pretend to be log format for everything. 268 | 3. No, it is not related to `log15` logger though `kiwi` shares the same logfmt format and some ideas with him. 269 | 4. It did not offer "Logger" interface because IMO interface is useless for loggers. It offers interfaces for the parts of logger like formatters. 270 | 271 | ## Similar works for structured logging 272 | 273 | * [zap](https://github.com/uber-go/zap) 274 | * [zerolog](https://github.com/rs/zerolog) 275 | * [logxi](https://github.com/mgutz/logxi) 276 | * [logrus](https://github.com/Sirupsen/logrus) 277 | * [log15](https://github.com/inconshreveable/log15) 278 | * [zlog](https://github.com/HardySimpson/zlog) - this one is even not in Go but this C logger has some similar ideas. 279 | 280 | ## Comparison with other loggers 281 | 282 | It is not the fastest logger among benchmarked but fast enough and careful about memory allocations. 283 | It much faster than `logrus` and `log15`. But slower than `logxi` tests. Need more detailed tests 284 | though. See the benchmarks results at 285 | [github.com/grafov/go-loggers-comparison](https://github.com/grafov/go-loggers-comparison). 286 | 287 | ## Roadmap 288 | 289 | What should be done before the first release: 290 | 291 | * handle nils in more convenient way 292 | * test behaviour of all the filters and fix possible bugs 293 | * test JSON output and fix possible bugs 294 | * ~~realize API for custom filters~~ 295 | 296 | Future plans: 297 | 298 | * ~~extend the predefined filters~~ (cancelled) 299 | * optional colour formatter for the console 300 | * throttling mode for sinks 301 | * ~~increase tests coverage up to 50%~~ 302 | * add tests for concurrent execution use cases 303 | * ~~increase tests coverage up to 75%~~ 304 | * multiline output for values 305 | * increase tests coverage up to 80% 306 | 307 | See details about tasks and ideas in `TODO.org` (orgmode format). 308 | See current [tests coverage](http://gocover.io/github.com/grafov/kiwi). 309 | 310 | ## Origins 311 | 312 | * logfmt description [brandur.org/logfmt](https://brandur.org/logfmt) 313 | * logfmt realization in Go and specs [godoc.org/github.com/kr/logfmt](https://godoc.org/github.com/kr/logfmt) 314 | * see also logfmt realization [github.com/go-logfmt/logfmt](https://github.com/go-logfmt/logfmt) 315 | * picture used for logo [openclipart.org/detail/4416/kiwi-bird](https://openclipart.org/detail/4416/kiwi-bird) 316 | -------------------------------------------------------------------------------- /kiwi-vs-other.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Logger instance 1 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Logger instance 2 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Logger instance 3 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | Writer 1 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | Writer 2 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | Writer 2 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | Logger instance 1 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | Logger instance 2 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | Logger instance 3 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | Filter 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | Filter 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | Filter 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | Writer 1 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | Writer 2 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | Writer 3 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | Common loggers logic :| Too straight and boring eh? 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | Kiwi logic... What? 8() 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | --------------------------------------------------------------------------------