├── example ├── .gitignore ├── curl-example-request.sh └── example.go ├── go.mod ├── logger_test.go ├── LICENSE ├── logBufferOptions_test.go ├── options_test.go ├── logBufferOptions.go ├── options.go ├── logBuffer.go ├── logger.go ├── middleware.go ├── logBuffer_test.go ├── README.md ├── middleware_test.go └── go.sum /example/.gitignore: -------------------------------------------------------------------------------- 1 | example -------------------------------------------------------------------------------- /example/curl-example-request.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## send an example request. 4 | curl http://localhost:29090 5 | 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Bose/go-gin-logrus/v2 2 | 3 | require ( 4 | github.com/Bose/go-gin-opentracing v1.0.3 5 | github.com/gin-gonic/gin v1.4.0 6 | github.com/google/uuid v1.1.0 7 | github.com/kr/pretty v0.1.0 // indirect 8 | github.com/matryer/is v1.2.0 9 | github.com/mitchellh/copystructure v1.0.0 10 | github.com/opentracing/opentracing-go v1.1.0 11 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect 12 | github.com/prometheus/common v0.2.0 // indirect 13 | github.com/prometheus/procfs v0.0.0-20190219184716-e4d4a2206da0 // indirect 14 | github.com/sirupsen/logrus v1.3.0 15 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package ginlogrus 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func TestNewBuffer(t *testing.T) { 15 | 16 | c := getTestContext("boo", "bar", true) 17 | logger := GetCtxLogger(c) 18 | logger.Info("now") 19 | 20 | buff := NewBuffer(logger) 21 | logger.Info("hey") 22 | if strings.Contains(buff.String(), "hey") == false || strings.Contains(buff.String(), "entries") == false { 23 | t.Errorf("Expected hey and found %v", buff.String()) 24 | } 25 | if strings.Contains(buff.String(), "now") { 26 | t.Errorf("didn't expeect 'now' and got %v", buff.String()) 27 | } 28 | 29 | c = getTestContext("boo", "bar", false) 30 | logger.Info("now") 31 | 32 | } 33 | 34 | func getTestContext(hdr string, v string, withAggregateLogger bool) *gin.Context { 35 | buf := new(bytes.Buffer) 36 | c, _ := gin.CreateTestContext(httptest.NewRecorder()) 37 | c.Request, _ = http.NewRequest("GET", "/", buf) 38 | c.Request.Header.Set(hdr, v) 39 | 40 | if withAggregateLogger { 41 | aggregateLoggingBuff := NewLogBuffer() 42 | aggregateRequestLogger := &logrus.Logger{ 43 | Out: &aggregateLoggingBuff, 44 | Formatter: new(logrus.JSONFormatter), 45 | Hooks: make(logrus.LevelHooks), 46 | Level: logrus.DebugLevel, 47 | } 48 | // you have to use this logger for every *logrus.Entry you create 49 | c.Set("aggregate-logger", aggregateRequestLogger) 50 | 51 | } 52 | return c 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Bose Corporation. 2 | Authored by Jim Lambert. 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 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (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. -------------------------------------------------------------------------------- /logBufferOptions_test.go: -------------------------------------------------------------------------------- 1 | package ginlogrus 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWithBanner(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | want bool 11 | }{ 12 | {name: "yes", want: true}, 13 | {name: "no", want: false}, 14 | } 15 | for _, tt := range tests { 16 | t.Run(tt.name, func(t *testing.T) { 17 | opts := defaultLogBufferOptions() 18 | f := WithBanner(tt.want) 19 | f(&opts) 20 | if opts.addBanner != tt.want { 21 | t.Errorf("WithBanner() = %v, want %v", opts.addBanner, tt.want) 22 | } 23 | }) 24 | } 25 | } 26 | 27 | func TestWithCustomBanner(t *testing.T) { 28 | tests := []struct { 29 | name string 30 | want string 31 | }{ 32 | {name: "custom", want: "CustomBanner"}, 33 | {name: "default", want: DefaultBanner}, 34 | } 35 | for _, tt := range tests { 36 | t.Run(tt.name, func(t *testing.T) { 37 | opts := defaultLogBufferOptions() 38 | f := WithCustomBanner(tt.want) 39 | f(&opts) 40 | if opts.banner != tt.want { 41 | t.Errorf("WithBanner() = %v, want %v", opts.addBanner, tt.want) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestWithHeader(t *testing.T) { 48 | tests := []struct { 49 | name string 50 | wantKey string 51 | wantValue bool 52 | }{ 53 | {name: "yes", wantKey: "yes", wantValue: true}, 54 | {name: "no", wantKey: "now", wantValue: false}, 55 | } 56 | for _, tt := range tests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | opts := defaultLogBufferOptions() 59 | f := WithHeader(tt.wantKey, tt.wantValue) 60 | f(&opts) 61 | if opts.withHeaders[tt.wantKey].(bool) != tt.wantValue { 62 | t.Errorf("WithBanner() = %v, want %v", opts.withHeaders[tt.wantKey].(bool), tt.wantValue) 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package ginlogrus 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "testing" 6 | ) 7 | 8 | func TestWithAggregateLogging(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | want bool 12 | }{ 13 | {name: "true", want: true}, 14 | {name: "false", want: false}, 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | opts := defaultOptions 19 | f := WithAggregateLogging(tt.want) 20 | f(&opts) 21 | if opts.aggregateLogging != tt.want { 22 | t.Errorf("WithAggregateLogging() = %v, want %v", opts.aggregateLogging, tt.want) 23 | } 24 | }) 25 | } 26 | } 27 | 28 | func TestWithEmptyAggregateEntries(t *testing.T) { 29 | tests := []struct { 30 | name string 31 | want bool 32 | }{ 33 | {name: "true", want: true}, 34 | {name: "false", want: false}, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | opts := defaultOptions 39 | f := WithEmptyAggregateEntries(tt.want) 40 | f(&opts) 41 | if opts.emptyAggregateEntries != tt.want { 42 | t.Errorf("WithEmptyAggregateEntries() = %v, want %v", opts.emptyAggregateEntries, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestWithLogLevel(t *testing.T) { 49 | tests := []struct { 50 | name string 51 | want logrus.Level 52 | }{ 53 | {name: "info", want: logrus.InfoLevel}, 54 | {name: "debug", want: logrus.DebugLevel}, 55 | {name: "error", want: logrus.ErrorLevel}, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | opts := defaultOptions 60 | f := WithLogLevel(tt.want) 61 | f(&opts) 62 | if opts.logLevel != tt.want { 63 | t.Errorf("WithLogLevel() = %v, want %v", opts.aggregateLogging, tt.want) 64 | } 65 | }) 66 | } 67 | } -------------------------------------------------------------------------------- /logBufferOptions.go: -------------------------------------------------------------------------------- 1 | package ginlogrus 2 | 3 | const DefaultBanner = "[GIN] --------------------------------------------------------------- GinLogrusWithTracing ----------------------------------------------------------------" 4 | 5 | // LogBufferOption - define options for LogBuffer 6 | type LogBufferOption func(*logBufferOptions) 7 | type logBufferOptions struct { 8 | addBanner bool 9 | withHeaders map[string]interface{} 10 | maxSize uint 11 | banner string 12 | } 13 | 14 | // DefaultLogBufferMaxSize - avg single spaced page contains 3k chars, so 100k == 33 pages which is a reasonable max 15 | const DefaultLogBufferMaxSize = 100000 16 | 17 | func defaultLogBufferOptions() logBufferOptions { 18 | return logBufferOptions{ 19 | maxSize: DefaultLogBufferMaxSize, 20 | banner: DefaultBanner, 21 | addBanner: false, 22 | } 23 | } 24 | 25 | // WithBanner - define an Option func for passing in an optional add Banner 26 | func WithBanner(a bool) LogBufferOption { 27 | return func(o *logBufferOptions) { 28 | o.addBanner = a 29 | } 30 | } 31 | 32 | // WithHeader - define an Option func for passing in a set of optional header 33 | func WithHeader(k string, v interface{}) LogBufferOption { 34 | return func(o *logBufferOptions) { 35 | if o.withHeaders == nil { 36 | o.withHeaders = make(map[string]interface{}) 37 | } 38 | o.withHeaders[k] = v 39 | } 40 | } 41 | 42 | // WithMaxSize specifies the bounded max size the logBuffer can grow to 43 | func WithMaxSize(s uint) LogBufferOption { 44 | return func(o *logBufferOptions) { 45 | o.maxSize = s 46 | } 47 | } 48 | 49 | // WithCustomBanner allows users to define their own custom banner 50 | func WithCustomBanner(b string) LogBufferOption { 51 | return func(o *logBufferOptions) { 52 | o.banner = b 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package ginlogrus 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // Option - define options for WithTracing() 12 | type Option func(*options) 13 | 14 | // Function definition for reduced logging. The return value of this function 15 | // will be used to determine whether or not a log will be output. 16 | type ReducedLoggingFunc func(c *gin.Context) bool 17 | 18 | type options struct { 19 | aggregateLogging bool 20 | logLevel logrus.Level 21 | emptyAggregateEntries bool 22 | reducedLoggingFunc ReducedLoggingFunc 23 | writer io.Writer 24 | banner string 25 | } 26 | 27 | // defaultOptions - some defs options to NewJWTCache() 28 | var defaultOptions = options{ 29 | aggregateLogging: false, 30 | logLevel: logrus.DebugLevel, 31 | emptyAggregateEntries: true, 32 | reducedLoggingFunc: func(c *gin.Context) bool { return true }, 33 | writer: os.Stdout, 34 | banner: DefaultBanner, 35 | } 36 | 37 | // WithAggregateLogging - define an Option func for passing in an optional aggregateLogging 38 | func WithAggregateLogging(a bool) Option { 39 | return func(o *options) { 40 | o.aggregateLogging = a 41 | } 42 | } 43 | 44 | // WithEmptyAggregateEntries - define an Option func for printing aggregate logs with empty entries 45 | func WithEmptyAggregateEntries(a bool) Option { 46 | return func(o *options) { 47 | o.emptyAggregateEntries = a 48 | } 49 | } 50 | 51 | // WithReducedLoggingFunc - define an Option func for reducing logs based on a custom function 52 | func WithReducedLoggingFunc(a ReducedLoggingFunc) Option { 53 | return func(o *options) { 54 | o.reducedLoggingFunc = a 55 | } 56 | } 57 | 58 | // WithLogLevel - define an Option func for passing in an optional logLevel 59 | func WithLogLevel(logLevel logrus.Level) Option { 60 | return func(o *options) { 61 | o.logLevel = logLevel 62 | } 63 | } 64 | 65 | // WithWriter allows users to define the writer used for middlware aggregagte logging, the default writer is os.Stdout 66 | func WithWriter(w io.Writer) Option { 67 | return func(o *options) { 68 | o.writer = w 69 | } 70 | } 71 | 72 | // WithLogCustomBanner allows users to define their own custom banner. There is some overlap with this name and the LogBufferOption.CustomBanner and yes, 73 | // they are related options, but I didn't want to make a breaking API change to support this new option... so we'll have to live with a bit of confusion/overlap in option names 74 | func WithLogCustomBanner(b string) Option { 75 | return func(o *options) { 76 | o.banner = b 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /logBuffer.go: -------------------------------------------------------------------------------- 1 | package ginlogrus 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/mitchellh/copystructure" 11 | ) 12 | 13 | // LogBuffer - implement io.Writer inferface to append to a string 14 | type LogBuffer struct { 15 | Buff strings.Builder 16 | header map[string]interface{} 17 | headerMU *sync.RWMutex 18 | AddBanner bool 19 | banner string 20 | MaxSize uint 21 | } 22 | 23 | // NewLogBuffer - create a LogBuffer and initialize it 24 | func NewLogBuffer(opt ...LogBufferOption) LogBuffer { 25 | opts := defaultLogBufferOptions() 26 | for _, o := range opt { 27 | o(&opts) 28 | } 29 | b := LogBuffer{ 30 | header: opts.withHeaders, 31 | headerMU: &sync.RWMutex{}, 32 | AddBanner: opts.addBanner, 33 | MaxSize: opts.maxSize, 34 | } 35 | b.SetCustomBanner(opts.banner) 36 | return b 37 | } 38 | 39 | // StoreHeader - store a header 40 | func (b *LogBuffer) StoreHeader(k string, v interface{}) { 41 | b.headerMU.Lock() 42 | if b.header == nil { 43 | b.header = make(map[string]interface{}) 44 | } 45 | b.header[k] = v 46 | b.headerMU.Unlock() 47 | } 48 | 49 | // DeleteHeader - delete a header 50 | func (b *LogBuffer) DeleteHeader(k string) { 51 | if b.header == nil { 52 | return 53 | } 54 | b.headerMU.Lock() 55 | delete(b.header, k) 56 | b.headerMU.Unlock() 57 | } 58 | 59 | // GetHeader - get a header 60 | func (b *LogBuffer) GetHeader(k string) (interface{}, bool) { 61 | if b.header == nil { 62 | return nil, false 63 | } 64 | b.headerMU.RLock() 65 | r, ok := b.header[k] 66 | b.headerMU.RUnlock() 67 | return r, ok 68 | } 69 | 70 | // GetAllHeaders - return all the headers 71 | func (b *LogBuffer) GetAllHeaders() (map[string]interface{}, error) { 72 | b.headerMU.RLock() 73 | dup, err := copystructure.Copy(b.header) 74 | b.headerMU.RUnlock() 75 | if err != nil { 76 | return nil, err 77 | } 78 | return dup.(map[string]interface{}), nil 79 | } 80 | 81 | // CopyHeader - copy a header 82 | func CopyHeader(dst *LogBuffer, src *LogBuffer) { 83 | src.headerMU.Lock() 84 | dup, err := copystructure.Copy(src.header) 85 | dupBanner := src.AddBanner 86 | src.headerMU.Unlock() 87 | 88 | dst.headerMU.Lock() 89 | if err != nil { 90 | dst.header = map[string]interface{}{} 91 | } else { 92 | dst.header = dup.(map[string]interface{}) 93 | } 94 | dst.AddBanner = dupBanner 95 | dst.headerMU.Unlock() 96 | } 97 | 98 | // Write - simply append to the strings.Buffer but add a comma too 99 | func (b *LogBuffer) Write(data []byte) (n int, err error) { 100 | newData := bytes.TrimSuffix(data, []byte("\n")) 101 | 102 | if len(newData)+b.Buff.Len() > int(b.MaxSize) { 103 | return 0, fmt.Errorf("write failed: buffer MaxSize = %d, current len = %d, attempted to write len = %d, data == %s", b.MaxSize, b.Buff.Len(), len(newData), newData) 104 | } 105 | return b.Buff.Write(append(newData, []byte(",")...)) 106 | } 107 | 108 | // Length - return the length of the aggregate log buffer 109 | func (b *LogBuffer) Length() int { 110 | return b.Buff.Len() 111 | } 112 | 113 | // String - output the strings.Builder as one aggregated JSON object 114 | func (b *LogBuffer) String() string { 115 | var str strings.Builder 116 | str.WriteString("{") 117 | if b.header != nil && len(b.header) != 0 { 118 | b.headerMU.RLock() 119 | hdr, err := json.Marshal(b.header) 120 | b.headerMU.RUnlock() 121 | if err != nil { 122 | fmt.Println("Error encoding logBuffer JSON") 123 | } 124 | str.Write(hdr[1 : len(hdr)-1]) 125 | str.WriteString(",") 126 | } 127 | str.WriteString("\"entries\":[" + strings.TrimSuffix(b.Buff.String(), ",") + "]") 128 | if b.AddBanner { 129 | str.WriteString(b.banner) 130 | } 131 | str.WriteString("}\n") 132 | return str.String() 133 | } 134 | 135 | // SetCustomBanner allows a custom banner to be set after the NewLogBuffer() has been used 136 | func (b *LogBuffer) SetCustomBanner(banner string) { 137 | b.banner = fmt.Sprintf(",\"banner\":\"%s\"", banner) 138 | } 139 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "runtime" 8 | "time" 9 | 10 | "github.com/opentracing/opentracing-go/ext" 11 | 12 | ginlogrus "github.com/Bose/go-gin-logrus" 13 | ginopentracing "github.com/Bose/go-gin-opentracing" 14 | "github.com/gin-gonic/gin" 15 | opentracing "github.com/opentracing/opentracing-go" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | func main() { 20 | // use the JSON formatter 21 | logrus.SetFormatter(&logrus.JSONFormatter{}) 22 | 23 | r := gin.New() // don't use the Default(), since it comes with a logger 24 | 25 | // setup tracing... 26 | hostName, err := os.Hostname() 27 | if err != nil { 28 | hostName = "unknown" 29 | } 30 | 31 | tracer, reporter, closer, err := ginopentracing.InitTracing( 32 | fmt.Sprintf("go-gin-logrus-example::%s", hostName), // service name for the traces 33 | "localhost:5775", // where to send the spans 34 | ginopentracing.WithEnableInfoLog(false)) // WithEnableLogInfo(false) will not log info on every span sent... if set to true it will log and they won't be aggregated 35 | if err != nil { 36 | panic("unable to init tracing") 37 | } 38 | defer closer.Close() 39 | defer reporter.Close() 40 | opentracing.SetGlobalTracer(tracer) 41 | 42 | p := ginopentracing.OpenTracer([]byte("api-request-")) 43 | r.Use(p) 44 | 45 | r.Use(gin.Recovery()) // add Recovery middleware 46 | useBanner := true 47 | useUTC := true 48 | r.Use(ginlogrus.WithTracing(logrus.StandardLogger(), 49 | useBanner, 50 | time.RFC3339, 51 | useUTC, 52 | "requestID", 53 | []byte("uber-trace-id"), // where jaeger might have put the trace id 54 | []byte("RequestID"), // where the trace ID might already be populated in the headers 55 | ginlogrus.WithAggregateLogging(true))) 56 | 57 | r.GET("/", func(c *gin.Context) { 58 | ginlogrus.SetCtxLoggerHeader(c, "new-header-index-name", "this is how you set new header level data") 59 | 60 | logger := ginlogrus.GetCtxLogger(c) // will get a logger with the aggregate Logger set if it's enabled - handy if you've already set fields for the request 61 | logger.Info("this will be aggregated into one write with the access log and will show up when the request is completed") 62 | 63 | // add some new fields to the existing logger 64 | logger = ginlogrus.SetCtxLogger(c, logger.WithFields(logrus.Fields{"comment": "this is an aggregated log entry with initial comment field"})) 65 | logger.Debug("aggregated entry with new comment field") 66 | 67 | // replace existing logger fields with new ones (notice it's logrus.WithFields()) 68 | logger = ginlogrus.SetCtxLogger(c, logrus.WithFields(logrus.Fields{"new-comment": "this is an aggregated log entry with reset comment field"})) 69 | logger.Error("aggregated error entry with new-comment field") 70 | 71 | logrus.Info("this will NOT be aggregated and will be logged immediately") 72 | span := newSpanFromContext(c, "sleep-span") 73 | defer span.Finish() // this will get logged because tracing was setup with ginopentracing.WithEnableInfoLog(true) 74 | 75 | go func() { 76 | // need a NewBuffer for aggregate logging of this goroutine (since the req will be done long before this thing finishes) 77 | // it will inherit header info from the existing request 78 | buff := ginlogrus.NewBuffer(logger) 79 | time.Sleep(1 * time.Second) 80 | logger.Info("Hi from a goroutine completing after the request") 81 | fmt.Printf(buff.String()) 82 | }() 83 | c.JSON(200, "Hello world!") 84 | }) 85 | 86 | if err := r.Run(":29090"); err != nil { 87 | log.Println("Run error: ", err) 88 | } 89 | } 90 | 91 | func newSpanFromContext(c *gin.Context, operationName string) opentracing.Span { 92 | parentSpan, _ := c.Get("tracing-context") 93 | options := []opentracing.StartSpanOption{ 94 | opentracing.Tag{Key: ext.SpanKindRPCServer.Key, Value: ext.SpanKindRPCServer.Value}, 95 | opentracing.Tag{Key: string(ext.HTTPMethod), Value: c.Request.Method}, 96 | opentracing.Tag{Key: string(ext.HTTPUrl), Value: c.Request.URL.Path}, 97 | opentracing.Tag{Key: "current-goroutines", Value: runtime.NumGoroutine()}, 98 | } 99 | 100 | if parentSpan != nil { 101 | options = append(options, opentracing.ChildOf(parentSpan.(opentracing.Span).Context())) 102 | } 103 | 104 | return opentracing.StartSpan(operationName, options...) 105 | } 106 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package ginlogrus 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/google/uuid" 8 | opentracing "github.com/opentracing/opentracing-go" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // SetCtxLoggerHeader - if aggregate logging, add header info... otherwise just info log the data passed 13 | func SetCtxLoggerHeader(c *gin.Context, name string, data interface{}) { 14 | logger := GetCtxLogger(c) 15 | _, found := c.Get("aggregate-logger") 16 | if found { 17 | logger.Logger.Out.(*LogBuffer).StoreHeader(name, data) 18 | } 19 | if !found { 20 | logger.Infof("%s: %v", name, data) 21 | } 22 | } 23 | 24 | // SetCtxLogger - used when you want to set the *logrus.Entry with new logrus.WithFields{} for this request in the gin.Context so it can be used going forward for the request 25 | func SetCtxLogger(c *gin.Context, logger *logrus.Entry) *logrus.Entry { 26 | log, found := c.Get("aggregate-logger") 27 | if found { 28 | logger.Logger = log.(*logrus.Logger) 29 | logger = logger.WithFields(logrus.Fields{}) // no need to add additional fields when aggregate logging 30 | } 31 | if !found { 32 | // not aggregate logging, so make sure to add some needed fields 33 | logger = logger.WithFields(logrus.Fields{ 34 | "requestID": CxtRequestID(c), 35 | "method": c.Request.Method, 36 | "path": c.Request.URL.Path}) 37 | } 38 | c.Set("ctxLogger", logger) 39 | return logger 40 | } 41 | 42 | // GetCtxLogger - get the *logrus.Entry for this request from the gin.Context 43 | func GetCtxLogger(c *gin.Context) *logrus.Entry { 44 | l, ok := c.Get("ctxLogger") 45 | if ok { 46 | return l.(*logrus.Entry) 47 | } 48 | var logger *logrus.Entry 49 | log, found := c.Get("aggregate-logger") 50 | if found { 51 | logger = logrus.WithFields(logrus.Fields{}) 52 | logger.Logger = log.(*logrus.Logger) 53 | } 54 | if !found { 55 | // not aggregate logging, so make sure to add some needed fields 56 | logger = logrus.WithFields(logrus.Fields{ 57 | "requestID": CxtRequestID(c), 58 | "method": c.Request.Method, 59 | "path": c.Request.URL.Path, 60 | }) 61 | } 62 | c.Set("ctxLogger", logger) 63 | return logger 64 | } 65 | 66 | // CxtRequestID - if not already set, then add logrus Field to the entry with the tracing ID for the request. 67 | // then return the trace/request id 68 | func CxtRequestID(c *gin.Context) string { 69 | // already setup, so we're done 70 | if id, found := c.Get("RequestID"); found == true { 71 | return id.(string) 72 | } 73 | 74 | // see if we're using github.com/Bose/go-gin-opentracing which will set a span in "tracing-context" 75 | if s, foundSpan := c.Get("tracing-context"); foundSpan { 76 | span := s.(opentracing.Span) 77 | requestID := fmt.Sprintf("%v", span) 78 | c.Set("RequestID", requestID) 79 | return requestID 80 | } 81 | 82 | // some other process might have stuck it in a header 83 | if len(ContextTraceIDField) != 0 { 84 | if s, ok := c.Get(ContextTraceIDField); ok { 85 | span := s.(opentracing.Span) 86 | requestID := fmt.Sprintf("%v", span) 87 | c.Set("RequestID", requestID) 88 | return requestID 89 | } 90 | } 91 | 92 | if requestID := c.Request.Header.Get("uber-trace-id"); len(requestID) != 0 { 93 | c.Set("RequestID", requestID) 94 | return requestID 95 | } 96 | 97 | // finally, just create a fake request id... 98 | requestID := uuid.New().String() 99 | c.Set("RequestID", requestID) 100 | return requestID 101 | } 102 | 103 | // GetCxtRequestID - dig the request ID out of the *logrus.Entry in the gin.Context 104 | func GetCxtRequestID(c *gin.Context) string { 105 | return CxtRequestID(c) 106 | } 107 | 108 | // NewBuffer - create a new aggregate logging buffer for the *logrus.Entry , which can be flushed by the consumer 109 | // how-to, when to use this: 110 | // the request level log entry is written when the request is over, so you need this thing to 111 | // write go routine logs that complete AFTER the request is completed. 112 | // careful: the loggers will share a ref to the same Header (writes to one will affect the other) 113 | // example: 114 | // go func() { 115 | // buff := NewBuffer(logger) // logger is an existing *logrus.Entry 116 | // // do somem work here and write some logs via the logger. Like logger.Info("hi mom! I'm a go routine that finished after the request") 117 | // fmt.Printf(buff.String()) // this will write the aggregated buffered logs to stdout 118 | // }() 119 | // 120 | func NewBuffer(l *logrus.Entry) *LogBuffer { 121 | buff := NewLogBuffer() 122 | if l, ok := l.Logger.Out.(*LogBuffer); ok { 123 | CopyHeader(&buff, l) 124 | buff.AddBanner = l.AddBanner 125 | } 126 | // buff.Header = l.Logger.Out.(*ginlogrus.LogBuffer).Header 127 | l.Logger = &logrus.Logger{ 128 | Out: &buff, 129 | Formatter: new(logrus.JSONFormatter), 130 | Hooks: make(logrus.LevelHooks), 131 | Level: logrus.DebugLevel, 132 | } 133 | return &buff 134 | } 135 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package ginlogrus 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | opentracing "github.com/opentracing/opentracing-go" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // ContextTraceIDField - used to find the trace id in the gin.Context - optional 13 | var ContextTraceIDField string 14 | 15 | type loggerEntryWithFields interface { 16 | WithFields(fields logrus.Fields) *logrus.Entry 17 | } 18 | 19 | // WithTracing returns a gin.HandlerFunc (middleware) that logs requests using logrus. 20 | // 21 | // Requests with errors are logged using logrus.Error(). 22 | // Requests without errors are logged using logrus.Info(). 23 | // 24 | // It receives: 25 | // 1. A logrus.Entry with fields 26 | // 2. A boolean stating whether to use a BANNER in the log entry 27 | // 3. A time package format string (e.g. time.RFC3339). 28 | // 4. A boolean stating whether to use UTC time zone or local. 29 | // 5. A string to use for Trace ID the Logrus log field. 30 | // 6. A []byte for the request header that contains the trace id 31 | // 7. A []byte for "getting" the requestID out of the gin.Context 32 | // 8. A list of possible ginlogrus.Options to apply 33 | func WithTracing( 34 | logger loggerEntryWithFields, 35 | useBanner bool, 36 | timeFormat string, 37 | utc bool, 38 | logrusFieldNameForTraceID string, 39 | traceIDHeader []byte, 40 | contextTraceIDField []byte, 41 | opt ...Option) gin.HandlerFunc { 42 | opts := defaultOptions 43 | 44 | for _, o := range opt { 45 | o(&opts) 46 | } 47 | if contextTraceIDField != nil { 48 | ContextTraceIDField = string(contextTraceIDField) 49 | } 50 | return func(c *gin.Context) { 51 | // var aggregateLoggingBuff strings.Builder 52 | // var aggregateLoggingBuff logBuffer 53 | aggregateLoggingBuff := NewLogBuffer(WithBanner(useBanner), WithCustomBanner(opts.banner)) 54 | aggregateRequestLogger := &logrus.Logger{ 55 | Out: &aggregateLoggingBuff, 56 | Formatter: new(logrus.JSONFormatter), 57 | Hooks: make(logrus.LevelHooks), 58 | Level: opts.logLevel, 59 | } 60 | 61 | start := time.Now() 62 | // some evil middlewares modify this values 63 | path := c.Request.URL.Path 64 | 65 | if opts.aggregateLogging { 66 | // you have to use this logger for every *logrus.Entry you create 67 | c.Set("aggregate-logger", aggregateRequestLogger) 68 | } 69 | c.Next() 70 | 71 | end := time.Now() 72 | latency := end.Sub(start) 73 | if utc { 74 | end = end.UTC() 75 | } 76 | 77 | var requestID string 78 | // see if we're using github.com/Bose/go-gin-opentracing which will set a span in "tracing-context" 79 | if s, foundSpan := c.Get("tracing-context"); foundSpan { 80 | span := s.(opentracing.Span) 81 | requestID = fmt.Sprintf("%v", span) 82 | } 83 | // check a user defined context field 84 | if len(requestID) == 0 && contextTraceIDField != nil { 85 | if id, ok := c.Get(string(ContextTraceIDField)); ok { 86 | requestID = id.(string) 87 | } 88 | } 89 | // okay.. finally check the request header 90 | if len(requestID) == 0 && traceIDHeader != nil { 91 | requestID = c.Request.Header.Get(string(traceIDHeader)) 92 | } 93 | 94 | comment := c.Errors.ByType(gin.ErrorTypePrivate).String() 95 | 96 | fields := logrus.Fields{ 97 | logrusFieldNameForTraceID: requestID, 98 | "status": c.Writer.Status(), 99 | "method": c.Request.Method, 100 | "path": path, 101 | "ip": c.ClientIP(), 102 | "latency-ms": float64(latency) / float64(time.Millisecond), 103 | "user-agent": c.Request.UserAgent(), 104 | "time": end.Format(timeFormat), 105 | "comment": comment, 106 | } 107 | if len(c.Errors) > 0 { 108 | entry := logger.WithFields(fields) 109 | // Append error field if this is an erroneous request. 110 | entry.Error(c.Errors.String()) 111 | } else { 112 | if gin.Mode() != gin.ReleaseMode && !opts.aggregateLogging { 113 | entry := logger.WithFields(fields) 114 | if useBanner { 115 | entry.Info("[GIN] --------------------------------------------------------------- GinLogrusWithTracing ----------------------------------------------------------------") 116 | } else { 117 | entry.Info() 118 | } 119 | } 120 | // If aggregate logging is enabled, check if we have entries to log or we are not omitting empty logs 121 | if opts.aggregateLogging { 122 | // If we are running structured logging, execute the reduced logging function(default to true) 123 | // if we pass the check, check if we have any entries to log or if we are logging empty entries (default to true) 124 | executeReduced := opts.reducedLoggingFunc(c) 125 | if executeReduced { 126 | if aggregateLoggingBuff.Length() > 0 || opts.emptyAggregateEntries { 127 | aggregateLoggingBuff.StoreHeader("request-summary-info", fields) 128 | fmt.Fprintf(opts.writer, aggregateLoggingBuff.String()) 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /logBuffer_test.go: -------------------------------------------------------------------------------- 1 | package ginlogrus 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "sync" 8 | "testing" 9 | ) 10 | 11 | func TestLogBuffer_String(t *testing.T) { 12 | 13 | tests := []struct { 14 | name string 15 | buff LogBuffer 16 | write []byte 17 | contains string 18 | }{ 19 | { 20 | name: "hey", 21 | buff: NewLogBuffer(WithBanner(true), WithHeader("id1", "val1"), WithHeader("id2", "id2")), 22 | write: []byte("\"msg\":\"hey-one\""), 23 | contains: "hey", 24 | }, 25 | { 26 | name: "hey-now", 27 | buff: NewLogBuffer(WithHeader("hey", "now")), 28 | write: []byte("\"msg\":\"hey-two\""), 29 | contains: "hey", 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | _, err := tt.buff.Write(tt.write) 35 | if err != nil { 36 | t.Error("LogBuffer.String() Write error: ", err) 37 | } 38 | fmt.Println("buff == ", tt.buff.String()) 39 | if !strings.Contains(tt.buff.String(), tt.contains) { 40 | t.Errorf("LogBuffer.String() = %v, want %v", tt.buff.String(), tt.contains) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestLogBuffer_Length(t *testing.T) { 47 | 48 | tests := []struct { 49 | name string 50 | buff LogBuffer 51 | write []byte 52 | expectedLength int 53 | }{ 54 | { 55 | name: "hey", 56 | buff: NewLogBuffer(WithBanner(true), WithHeader("id1", "val1"), WithHeader("id2", "id2")), 57 | write: []byte("\"msg\":\"hey-one\""), 58 | expectedLength: 16, 59 | }, 60 | { 61 | name: "hey-now", 62 | buff: NewLogBuffer(WithHeader("hey", "now")), 63 | write: []byte("\"msg\":\"hey-two\""), 64 | expectedLength: 16, 65 | }, 66 | { 67 | name: "hey-now", 68 | buff: NewLogBuffer(WithHeader("hey", "now")), 69 | write: []byte("\"msg\":\"hey-three\",\"msg\":\"hey-four\""), 70 | expectedLength: 35, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | _, err := tt.buff.Write(tt.write) 76 | if err != nil { 77 | t.Error("LogBuffer.String() Write error: ", err) 78 | } 79 | fmt.Println("buff == ", tt.buff.String()) 80 | 81 | if tt.buff.Length() != tt.expectedLength { 82 | t.Errorf("LogBuffer.Length() = %v, want %v", tt.buff.Length(), tt.expectedLength) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | func TestNewLogBuffer(t *testing.T) { 89 | tests := []struct { 90 | name string 91 | opt []LogBufferOption 92 | want LogBuffer 93 | }{ 94 | { 95 | name: "one", 96 | opt: []LogBufferOption{WithBanner(true), WithHeader("1", true)}, 97 | want: LogBuffer{AddBanner: true, header: map[string]interface{}{"1": true}, headerMU: &sync.RWMutex{}, MaxSize: DefaultLogBufferMaxSize, banner: ",\"banner\":\"[GIN] --------------------------------------------------------------- GinLogrusWithTracing ----------------------------------------------------------------\""}, 98 | }, 99 | { 100 | name: "two", 101 | opt: []LogBufferOption{WithHeader("1", "one"), WithHeader("2", true)}, 102 | want: LogBuffer{AddBanner: false, header: map[string]interface{}{"1": "one", "2": true}, headerMU: &sync.RWMutex{}, MaxSize: DefaultLogBufferMaxSize, banner: ",\"banner\":\"[GIN] --------------------------------------------------------------- GinLogrusWithTracing ----------------------------------------------------------------\""}, 103 | }, 104 | { 105 | name: "three", 106 | opt: []LogBufferOption{WithBanner(true), WithHeader("1", true), WithCustomBanner("custom")}, 107 | want: LogBuffer{AddBanner: true, header: map[string]interface{}{"1": true}, headerMU: &sync.RWMutex{}, MaxSize: DefaultLogBufferMaxSize, banner: ",\"banner\":\"custom\""}, 108 | }, 109 | { 110 | name: "four", 111 | opt: []LogBufferOption{WithBanner(false), WithHeader("1", true)}, 112 | want: LogBuffer{AddBanner: false, header: map[string]interface{}{"1": true}, headerMU: &sync.RWMutex{}, MaxSize: DefaultLogBufferMaxSize, banner: ",\"banner\":\"[GIN] --------------------------------------------------------------- GinLogrusWithTracing ----------------------------------------------------------------\""}, 113 | }, 114 | } 115 | for _, tt := range tests { 116 | t.Run(tt.name, func(t *testing.T) { 117 | if got := NewLogBuffer(tt.opt...); !reflect.DeepEqual(got, tt.want) { 118 | t.Errorf("NewLogBuffer() = %v, want %v", got, tt.want) 119 | } 120 | }) 121 | } 122 | } 123 | 124 | func TestLogBuffer_StoreHeader_DeleteHeader_GetHeader_GetAllHeaders_CopyHeader(t *testing.T) { 125 | tests := []struct { 126 | name string 127 | buff LogBuffer 128 | k string 129 | v interface{} 130 | contains string 131 | }{ 132 | { 133 | name: "1", 134 | buff: NewLogBuffer(WithBanner(true)), 135 | k: "test-hdr", 136 | v: "test-value", 137 | contains: "test-value", 138 | }, 139 | { 140 | name: "2", 141 | buff: NewLogBuffer(WithBanner(true)), 142 | k: "test-hdr-2", 143 | v: true, 144 | contains: "test-hdr-2", 145 | }, 146 | } 147 | for _, tt := range tests { 148 | t.Run(tt.name, func(t *testing.T) { 149 | tt.buff.StoreHeader(tt.k, tt.v) 150 | if !strings.Contains(tt.buff.String(), tt.contains) { 151 | t.Errorf("expected %v and got %v", tt.contains, tt.buff.String()) 152 | } 153 | if got, _ := tt.buff.GetHeader(tt.k); !reflect.DeepEqual(got, tt.v) { 154 | t.Errorf("GetHeader() = %v, want %v", got, tt.v) 155 | } 156 | got, _ := tt.buff.GetAllHeaders() 157 | if reflect.DeepEqual(got, tt.v) { 158 | t.Errorf("GetAllHeaders() = %v, want %v", got, tt.v) 159 | } 160 | newBuff := NewLogBuffer() 161 | CopyHeader(&newBuff, &tt.buff) 162 | if reflect.DeepEqual(newBuff, tt.buff) != true { 163 | t.Errorf("CopyHeader() = %v, want %v", newBuff, tt.buff) 164 | } 165 | tt.buff.DeleteHeader(tt.k) 166 | if strings.Contains(tt.buff.String(), tt.contains) { 167 | t.Errorf("expected %v to be deleted and got %v", tt.contains, tt.buff.String()) 168 | } 169 | 170 | }) 171 | } 172 | } 173 | 174 | func TestLogBuffer_Write(t *testing.T) { 175 | tests := []struct { 176 | name string 177 | b LogBuffer 178 | data []byte 179 | wantN int 180 | wantErr bool 181 | }{ 182 | { 183 | name: "1-fail", 184 | b: NewLogBuffer(WithMaxSize(1)), 185 | data: []byte("test write"), 186 | wantN: 0, 187 | wantErr: true, 188 | }, 189 | { 190 | name: "2-success", 191 | b: NewLogBuffer(WithMaxSize(100)), 192 | data: []byte("test write"), 193 | wantN: len("test write") + 1, 194 | wantErr: false, 195 | }, 196 | { 197 | name: "3-fail", 198 | b: NewLogBuffer(WithMaxSize(100)), 199 | data: []byte(tooBigBuff), 200 | wantN: 0, 201 | wantErr: true, 202 | }, 203 | } 204 | for _, tt := range tests { 205 | t.Run(tt.name, func(t *testing.T) { 206 | gotN, err := tt.b.Write(tt.data) 207 | if err != nil { 208 | t.Log("LogBuffer.Write() error == ", err) 209 | } 210 | if (err != nil) != tt.wantErr { 211 | t.Errorf("LogBuffer.Write() error = %v, wantErr %v", err, tt.wantErr) 212 | return 213 | } 214 | if gotN != tt.wantN { 215 | t.Errorf("LogBuffer.Write() = %v, want %v", gotN, tt.wantN) 216 | } 217 | }) 218 | } 219 | } 220 | 221 | var tooBigBuff = strings.Repeat("#", DefaultLogBufferMaxSize) + "1" 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-gin-logrus 2 | [![](https://godoc.org/github.com/Bose/go-gin-logrus?status.svg)](https://godoc.org/github.com/Bose/go-gin-logrus) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/Bose/go-gin-logrus)](https://goreportcard.com/report/github.com/Bose/go-gin-logrus) 4 | [![Release](https://img.shields.io/github/release/Bose/go-gin-logrus.svg?style=flat-square)](https://Bose/go-gin-logrus/releases) 5 | 6 | Gin Web Framework Open Tracing middleware. 7 | 8 | This middleware also support aggregate logging: the ability to aggregate all log entries into just one write. This aggregation is helpful when your logs are being sent to Kibana and you only want to index one log per request. 9 | 10 | ## Installation 11 | 12 | `$ go get github.com/Bose/go-gin-logrus` 13 | 14 | If you want to use it with opentracing you could consider installing: 15 | 16 | `$ go get github.com/Bose/go-gin-opentracing` 17 | 18 | ## Dependencies - for local development 19 | If you want to see your traces on your local system, you'll need to run a tracing backend like Jaeger. You'll find info about how-to in the [Jaeger Tracing github repo docs](https://github.com/jaegertracing/documentation/blob/master/content/docs/getting-started.md) 20 | Basically, you can run the Jaeger opentracing backend under docker via: 21 | 22 | ```bash 23 | docker run -d -e \ 24 | COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ 25 | -p 5775:5775/udp \ 26 | -p 6831:6831/udp \ 27 | -p 6832:6832/udp \ 28 | -p 5778:5778 \ 29 | -p 16686:16686 \ 30 | -p 14268:14268 \ 31 | -p 9411:9411 \ 32 | jaegertracing/all-in-one:latest 33 | ``` 34 | ## Usage 35 | ``` 36 | # example aggregated log entry for a request with UseBanner == true 37 | { 38 | "new-header-index-name": "this is how you set new header level data", 39 | "request-summary-info": { 40 | "comment": "", 41 | "ip": "::1", 42 | "latency": " 98.217µs", 43 | "method": "GET", 44 | "path": "/", 45 | "requestID": "4b4fb22ef51cc540:4b4fb22ef51cc540:0:1", 46 | "status": 200, 47 | "time": "2019-02-06T13:24:06Z", 48 | "user-agent": "curl/7.54.0" 49 | }, 50 | "entries": [ 51 | { 52 | "level": "info", 53 | "msg": "this will be aggregated into one write with the access log and will show up when the request is completed", 54 | "time": "2019-02-06T08:24:06-05:00" 55 | }, 56 | { 57 | "comment": "this is an aggregated log entry with initial comment field", 58 | "level": "debug", 59 | "msg": "aggregated entry with new comment field", 60 | "time": "2019-02-06T08:24:06-05:00" 61 | }, 62 | { 63 | "level": "error", 64 | "msg": "aggregated error entry with new-comment field", 65 | "new-comment": "this is an aggregated log entry with reset comment field", 66 | "time": "2019-02-06T08:24:06-05:00" 67 | } 68 | ], 69 | "banner": "[GIN] --------------------------------------------------------------- GinLogrusWithTracing ----------------------------------------------------------------" 70 | } 71 | 72 | ``` 73 | 74 | ```go 75 | package main 76 | 77 | import ( 78 | "fmt" 79 | "os" 80 | "runtime" 81 | "time" 82 | 83 | "github.com/opentracing/opentracing-go/ext" 84 | 85 | ginlogrus "github.com/Bose/go-gin-logrus" 86 | ginopentracing "github.com/Bose/go-gin-opentracing" 87 | "github.com/gin-gonic/gin" 88 | opentracing "github.com/opentracing/opentracing-go" 89 | "github.com/sirupsen/logrus" 90 | ) 91 | 92 | func main() { 93 | // use the JSON formatter 94 | logrus.SetFormatter(&logrus.JSONFormatter{}) 95 | 96 | r := gin.New() // don't use the Default(), since it comes with a logger 97 | 98 | // setup tracing... 99 | hostName, err := os.Hostname() 100 | if err != nil { 101 | hostName = "unknown" 102 | } 103 | 104 | tracer, reporter, closer, err := ginopentracing.InitTracing( 105 | fmt.Sprintf("go-gin-logrus-example::%s", hostName), // service name for the traces 106 | "localhost:5775", // where to send the spans 107 | ginopentracing.WithEnableInfoLog(false)) // WithEnableLogInfo(false) will not log info on every span sent... if set to true it will log and they won't be aggregated 108 | if err != nil { 109 | panic("unable to init tracing") 110 | } 111 | defer closer.Close() 112 | defer reporter.Close() 113 | opentracing.SetGlobalTracer(tracer) 114 | 115 | p := ginopentracing.OpenTracer([]byte("api-request-")) 116 | r.Use(p) 117 | 118 | r.Use(gin.Recovery()) // add Recovery middleware 119 | useBanner := true 120 | useUTC := true 121 | r.Use(ginlogrus.WithTracing(logrus.StandardLogger(), 122 | useBanner, 123 | time.RFC3339, 124 | useUTC, 125 | "requestID", 126 | []byte("uber-trace-id"), // where jaeger might have put the trace id 127 | []byte("RequestID"), // where the trace ID might already be populated in the headers 128 | ginlogrus.WithAggregateLogging(true))) 129 | 130 | r.GET("/", func(c *gin.Context) { 131 | ginlogrus.SetCtxLoggerHeader(c, "new-header-index-name", "this is how you set new header level data") 132 | 133 | logger := ginlogrus.GetCtxLogger(c) // will get a logger with the aggregate Logger set if it's enabled - handy if you've already set fields for the request 134 | logger.Info("this will be aggregated into one write with the access log and will show up when the request is completed") 135 | 136 | // add some new fields to the existing logger 137 | logger = ginlogrus.SetCtxLogger(c, logger.WithFields(logrus.Fields{"comment": "this is an aggregated log entry with initial comment field"})) 138 | logger.Debug("aggregated entry with new comment field") 139 | 140 | // replace existing logger fields with new ones (notice it's logrus.WithFields()) 141 | logger = ginlogrus.SetCtxLogger(c, logrus.WithFields(logrus.Fields{"new-comment": "this is an aggregated log entry with reset comment field"})) 142 | logger.Error("aggregated error entry with new-comment field") 143 | 144 | logrus.Info("this will NOT be aggregated and will be logged immediately") 145 | span := newSpanFromContext(c, "sleep-span") 146 | defer span.Finish() // this will get logged because tracing was setup with ginopentracing.WithEnableInfoLog(true) 147 | 148 | go func() { 149 | // need a NewBuffer for aggregate logging of this goroutine (since the req will be done long before this thing finishes) 150 | // it will inherit header info from the existing request 151 | buff := ginlogrus.NewBuffer(logger) 152 | time.Sleep(1 * time.Second) 153 | logger.Info("Hi from a goroutine completing after the request") 154 | fmt.Printf(buff.String()) 155 | }() 156 | c.JSON(200, "Hello world!") 157 | }) 158 | 159 | r.Run(":29090") 160 | } 161 | 162 | func newSpanFromContext(c *gin.Context, operationName string) opentracing.Span { 163 | parentSpan, _ := c.Get("tracing-context") 164 | options := []opentracing.StartSpanOption{ 165 | opentracing.Tag{Key: ext.SpanKindRPCServer.Key, Value: ext.SpanKindRPCServer.Value}, 166 | opentracing.Tag{Key: string(ext.HTTPMethod), Value: c.Request.Method}, 167 | opentracing.Tag{Key: string(ext.HTTPUrl), Value: c.Request.URL.Path}, 168 | opentracing.Tag{Key: "current-goroutines", Value: runtime.NumGoroutine()}, 169 | } 170 | 171 | if parentSpan != nil { 172 | options = append(options, opentracing.ChildOf(parentSpan.(opentracing.Span).Context())) 173 | } 174 | 175 | return opentracing.StartSpan(operationName, options...) 176 | } 177 | 178 | 179 | ``` 180 | 181 | See the [example.go file](https://github.com/Bose/go-gin-logrus/blob/master/example/example.go) 182 | 183 | ## Reduced Logging Options 184 | The Options.WithReducedLoggingFunc(c *gin.Context) allows users to specify a function for determining whether or not logs will be written. This function can be used with aggregate logging in situations where users want to maintain the details and fidelity of log messages but not necessarily log on every single request. The example below allows users to maintain aggregate logs at the DEBUG level but only write logs out on non-2xx response codes. 185 | Reduced Logging Function: 186 | ``` go 187 | // This function will determine whether to write a log message or not. 188 | // When the request is not a 2xx the function will return true indicating that a log message should be written. 189 | func ProductionLogging(c *gin.Context) bool { 190 | statusCode := c.Writer.Status() 191 | if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { 192 | return true 193 | } 194 | return false 195 | } 196 | ``` 197 | 198 | ``` go 199 | r.Use(ginlogrus.WithTracing(logrus.StandardLogger(), 200 | useBanner, 201 | time.RFC3339, 202 | useUTC, 203 | "requestID", 204 | []byte("uber-trace-id"), // where jaeger might have put the trace id 205 | []byte("RequestID"), // where the trace ID might already be populated in the headers 206 | ginlogrus.WithAggregateLogging(true), 207 | ginlogrus.WithReducedLoggingFunc(ProductionLogging))) 208 | ``` 209 | 210 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package ginlogrus 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/matryer/is" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func performRequest(method, target string, router *gin.Engine) *httptest.ResponseRecorder { 17 | r := httptest.NewRequest(method, target, nil) 18 | w := httptest.NewRecorder() 19 | router.ServeHTTP(w, r) 20 | return w 21 | } 22 | 23 | func TestNoLogMessageWithEmptyAggregateEntries(t *testing.T) { 24 | is := is.New(t) 25 | buff := "" 26 | getHandler := func(c *gin.Context) { 27 | SetCtxLoggerHeader(c, "EmptyEntries", "Nothing should be printed") 28 | 29 | logger := GetCtxLogger(c) 30 | logger.Info("test-entry-1") 31 | logger.Info("test-entry-2") 32 | c.JSON(200, "Hello world!") 33 | } 34 | gin.SetMode(gin.DebugMode) 35 | gin.DisableConsoleColor() 36 | 37 | l := bytes.NewBufferString(buff) 38 | r := gin.Default() 39 | r.Use(WithTracing(logrus.StandardLogger(), 40 | false, 41 | time.RFC3339, 42 | true, 43 | "requestID", 44 | []byte("uber-trace-id"), // where jaeger might have put the trace id 45 | []byte("RequestID"), // where the trace ID might already be populated in the headers 46 | WithAggregateLogging(true), 47 | WithEmptyAggregateEntries(false), 48 | WithLogLevel(logrus.WarnLevel), 49 | WithWriter(l))) 50 | r.GET("/", getHandler) 51 | w := performRequest("GET", "/", r) 52 | is.Equal(200, w.Code) 53 | t.Log("this is the buffer: ", l) 54 | is.True(len(l.String()) == 0) 55 | } 56 | 57 | func TestLogMessageWithEmptyAggregateEntriesAboveLogLevel(t *testing.T) { 58 | is := is.New(t) 59 | buff := "" 60 | getHandler := func(c *gin.Context) { 61 | SetCtxLoggerHeader(c, "AggregateEntries", "Shouldnt have messages below WARN") 62 | 63 | logger := GetCtxLogger(c) 64 | logger.Info("test-entry-1") 65 | logger.Info("test-entry-2") 66 | logger.Error("error-entry-1") 67 | c.JSON(200, "Hello world!") 68 | } 69 | gin.SetMode(gin.DebugMode) 70 | gin.DisableConsoleColor() 71 | 72 | l := bytes.NewBufferString(buff) 73 | r := gin.Default() 74 | r.Use(WithTracing(logrus.StandardLogger(), 75 | false, 76 | time.RFC3339, 77 | true, 78 | "requestID", 79 | []byte("uber-trace-id"), // where jaeger might have put the trace id 80 | []byte("RequestID"), // where the trace ID might already be populated in the headers 81 | WithAggregateLogging(true), 82 | WithEmptyAggregateEntries(false), 83 | WithLogLevel(logrus.WarnLevel), 84 | WithWriter(l))) 85 | r.GET("/", getHandler) 86 | w := performRequest("GET", "/", r) 87 | is.Equal(200, w.Code) 88 | t.Log("this is the buffer: ", l) 89 | is.True(len(l.String()) > 0) 90 | is.True(!strings.Contains(l.String(), "test-entry-1")) 91 | is.True(!strings.Contains(l.String(), "test-entry-2")) 92 | is.True(strings.Contains(l.String(), "error-entry-1")) 93 | } 94 | 95 | func TestBanner(t *testing.T) { 96 | is := is.New(t) 97 | buff := "" 98 | getHandler := func(c *gin.Context) { 99 | SetCtxLoggerHeader(c, "new-header-index-name", "this is how you set new header level data") 100 | 101 | logger := GetCtxLogger(c) 102 | logger.Info("test-entry-1") 103 | logger.Info("test-entry-2") 104 | c.JSON(200, "Hello world!") 105 | } 106 | gin.SetMode(gin.DebugMode) 107 | gin.DisableConsoleColor() 108 | 109 | l := bytes.NewBufferString(buff) 110 | r := gin.Default() 111 | r.Use(WithTracing(logrus.StandardLogger(), 112 | false, 113 | time.RFC3339, 114 | true, 115 | "requestID", 116 | []byte("uber-trace-id"), // where jaeger might have put the trace id 117 | []byte("RequestID"), // where the trace ID might already be populated in the headers 118 | WithAggregateLogging(true), 119 | WithWriter(l))) 120 | r.GET("/", getHandler) 121 | w := performRequest("GET", "/", r) 122 | is.Equal(200, w.Code) 123 | t.Log("this is the buffer: ", l) 124 | is.True(!strings.Contains(l.String(), "GinLogrusWithTracing")) 125 | 126 | buff = "" 127 | l = bytes.NewBufferString(buff) 128 | r = gin.New() 129 | r.Use(WithTracing(logrus.StandardLogger(), 130 | true, 131 | time.RFC3339, 132 | true, 133 | "requestID", 134 | []byte("uber-trace-id"), // where jaeger might have put the trace id 135 | []byte("RequestID"), // where the trace ID might already be populated in the headers 136 | WithAggregateLogging(true), 137 | WithWriter(l))) 138 | r.GET("/", getHandler) 139 | w = performRequest("GET", "/", r) 140 | is.Equal(200, w.Code) 141 | t.Log("this is the buffer: ", l) 142 | is.True(strings.Contains(l.String(), "GinLogrusWithTracing")) 143 | 144 | customBanner := "---- custom banner ----" 145 | buff = "" 146 | l = bytes.NewBufferString(buff) 147 | r = gin.New() 148 | r.Use(WithTracing(logrus.StandardLogger(), 149 | true, 150 | time.RFC3339, 151 | true, 152 | "requestID", 153 | []byte("uber-trace-id"), // where jaeger might have put the trace id 154 | []byte("RequestID"), // where the trace ID might already be populated in the headers 155 | WithLogCustomBanner(customBanner), 156 | WithAggregateLogging(true), 157 | WithWriter(l))) 158 | r.GET("/", getHandler) 159 | w = performRequest("GET", "/", r) 160 | is.Equal(200, w.Code) 161 | t.Log("this is the buffer: ", l) 162 | is.True(strings.Contains(l.String(), customBanner)) 163 | 164 | } 165 | 166 | func TestLogMessageWithProductionLevelReducedLogging(t *testing.T) { 167 | is := is.New(t) 168 | buff := "" 169 | getHandler := func(c *gin.Context) { 170 | SetCtxLoggerHeader(c, "ReducedLogging", "Shouldn't have messages with a 2xx response") 171 | 172 | logger := GetCtxLogger(c) 173 | logger.Info("test-entry-1") 174 | logger.Info("test-entry-2") 175 | logger.Error("error-entry-1") 176 | c.JSON(200, "Hello world!") 177 | } 178 | failHandler := func(c *gin.Context) { 179 | SetCtxLoggerHeader(c, "ReducedLogging", "Shouldn't have messages with a 2xx response") 180 | logger := GetCtxLogger(c) 181 | logger.Info("test-entry-1") 182 | logger.Info("test-entry-2") 183 | logger.Error("error-entry-1") 184 | c.JSON(401, "Hello fail!") 185 | } 186 | gin.SetMode(gin.DebugMode) 187 | gin.DisableConsoleColor() 188 | 189 | l := bytes.NewBufferString(buff) 190 | r := gin.Default() 191 | r.Use(WithTracing(logrus.StandardLogger(), 192 | false, 193 | time.RFC3339, 194 | true, 195 | "requestID", 196 | []byte("uber-trace-id"), // where jaeger might have put the trace id 197 | []byte("RequestID"), // where the trace ID might already be populated in the headers 198 | WithAggregateLogging(true), 199 | WithWriter(l), 200 | WithReducedLoggingFunc(productionLoggingTestFunc), 201 | )) 202 | r.GET("/", getHandler) 203 | r.GET("/fail", failHandler) 204 | w := performRequest("GET", "/", r) 205 | is.Equal(200, w.Code) 206 | t.Log("this is the buffer: ", l) 207 | // Beacuase the request is a 2xx we will not have any log entries including possible errors 208 | is.True(len(l.String()) == 0) 209 | is.True(!strings.Contains(l.String(), "test-entry-1")) 210 | is.True(!strings.Contains(l.String(), "test-entry-2")) 211 | is.True(!strings.Contains(l.String(), "error-entry-1")) 212 | 213 | w = performRequest("GET", "/fail", r) 214 | is.Equal(401, w.Code) 215 | t.Log("this is the buffer: ", l) 216 | // Beacuase the request is a 401 we will have all log entries including info logs 217 | is.True(len(l.String()) > 0) 218 | is.True(strings.Contains(l.String(), "test-entry-1")) 219 | is.True(strings.Contains(l.String(), "test-entry-2")) 220 | is.True(strings.Contains(l.String(), "error-entry-1")) 221 | 222 | } 223 | 224 | func TestLogMessageWithProductionReducedLoggingWarnLevel(t *testing.T) { 225 | is := is.New(t) 226 | buff := "" 227 | getHandler := func(c *gin.Context) { 228 | SetCtxLoggerHeader(c, "ReducedLogging", "Shouldn't have messages with a 2xx response") 229 | 230 | logger := GetCtxLogger(c) 231 | logger.Info("test-entry-1") 232 | logger.Info("test-entry-2") 233 | logger.Error("error-entry-1") 234 | c.JSON(200, "Hello world!") 235 | } 236 | failHandler := func(c *gin.Context) { 237 | SetCtxLoggerHeader(c, "ReducedLogging", "Shouldn't have messages with a 2xx response") 238 | logger := GetCtxLogger(c) 239 | logger.Info("test-entry-1") 240 | logger.Info("test-entry-2") 241 | logger.Error("error-entry-1") 242 | c.JSON(401, "Hello fail!") 243 | } 244 | gin.SetMode(gin.DebugMode) 245 | gin.DisableConsoleColor() 246 | 247 | l := bytes.NewBufferString(buff) 248 | r := gin.Default() 249 | r.Use(WithTracing(logrus.StandardLogger(), 250 | false, 251 | time.RFC3339, 252 | true, 253 | "requestID", 254 | []byte("uber-trace-id"), // where jaeger might have put the trace id 255 | []byte("RequestID"), // where the trace ID might already be populated in the headers 256 | WithAggregateLogging(true), 257 | WithWriter(l), 258 | WithLogLevel(logrus.WarnLevel), 259 | WithReducedLoggingFunc(productionLoggingTestFunc), 260 | )) 261 | r.GET("/", getHandler) 262 | r.GET("/fail", failHandler) 263 | w := performRequest("GET", "/", r) 264 | is.Equal(200, w.Code) 265 | t.Log("this is the buffer: ", l) 266 | // Beacuase the request is a 2xx we will not have any log entries including possible errors 267 | is.True(len(l.String()) == 0) 268 | is.True(!strings.Contains(l.String(), "test-entry-1")) 269 | is.True(!strings.Contains(l.String(), "test-entry-2")) 270 | is.True(!strings.Contains(l.String(), "error-entry-1")) 271 | 272 | w = performRequest("GET", "/fail", r) 273 | is.Equal(401, w.Code) 274 | t.Log("this is the buffer: ", l) 275 | // Beacuase the request is a 401 we will have log entries but because we have our log level at WARN we will not have info logs 276 | is.True(len(l.String()) > 0) 277 | is.True(!strings.Contains(l.String(), "test-entry-1")) 278 | is.True(!strings.Contains(l.String(), "test-entry-2")) 279 | is.True(strings.Contains(l.String(), "error-entry-1")) 280 | 281 | } 282 | 283 | // Same production logging function that will only log on statusCodes in a certain range 284 | func productionLoggingTestFunc(c *gin.Context) bool { 285 | statusCode := c.Writer.Status() 286 | if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices { 287 | return true 288 | } 289 | return false 290 | } 291 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Bose/go-gin-opentracing v1.0.3 h1:AiWAtYGIkKDYEtPX6wjHbxyOZ4sfjnAU1GFmtfjouks= 2 | github.com/Bose/go-gin-opentracing v1.0.3/go.mod h1:MRjPy7yY92/G4L9B1b1YGSIuSGpevD20ew0g4pMOiZk= 3 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= 6 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 7 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= 8 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74 h1:FaI7wNyesdMBSkIRVUuEEYEvmzufs7EqQvRAxfEXGbQ= 13 | github.com/gin-contrib/sse v0.0.0-20190125020943-a7658810eb74/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 14 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= 15 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 16 | github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= 17 | github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 18 | github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= 19 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 20 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 21 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 22 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 23 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 24 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 25 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 27 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 28 | github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s= 29 | github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 30 | github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= 31 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 32 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= 33 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 34 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 35 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 36 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 37 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 38 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 39 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 40 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 41 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 42 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 43 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 44 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 45 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 46 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 47 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= 48 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 49 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 50 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 51 | github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= 52 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 53 | github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= 54 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 57 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 58 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 59 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 60 | github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= 61 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 62 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 63 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 64 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 68 | github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= 69 | github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 70 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 71 | github.com/prometheus/client_model v0.0.0-20190109181635-f287a105a20e h1:/F8S20P9KteTOlxM8k6xWtTiY+u32wemAL2/zilHKzw= 72 | github.com/prometheus/client_model v0.0.0-20190109181635-f287a105a20e/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 73 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= 74 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 75 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 76 | github.com/prometheus/common v0.0.0-20190107103113-2998b132700a h1:bLKgQQEViHvsdgCwCGyyga8npETKygQ8b7c/28mJ8tw= 77 | github.com/prometheus/common v0.0.0-20190107103113-2998b132700a/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 78 | github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= 79 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 80 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 81 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 82 | github.com/prometheus/procfs v0.0.0-20190104112138-b1a0a9a36d74 h1:d1Xoc24yp/pXmWl2leBiBA+Tptce6cQsA+MMx/nOOcY= 83 | github.com/prometheus/procfs v0.0.0-20190104112138-b1a0a9a36d74/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 84 | github.com/prometheus/procfs v0.0.0-20190219184716-e4d4a2206da0 h1:4+Tdy73otddqWxwK30bAMLH9ymeHQ1Y5+fmSoCF1XtU= 85 | github.com/prometheus/procfs v0.0.0-20190219184716-e4d4a2206da0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 86 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 87 | github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= 88 | github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 89 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 90 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 91 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 92 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 93 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 94 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 95 | github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo= 96 | github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= 97 | github.com/uber/jaeger-client-go v2.15.0+incompatible h1:NP3qsSqNxh8VYr956ur1N/1C1PjvOJnJykCzcD5QHbk= 98 | github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= 99 | github.com/uber/jaeger-lib v1.5.0 h1:OHbgr8l656Ub3Fw5k9SWnBfIEwvoHQ+W2y+Aa9D1Uyo= 100 | github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= 101 | github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= 102 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 103 | go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= 104 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 105 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= 106 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 107 | golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M= 108 | golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 109 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 110 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 111 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 112 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 113 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY= 114 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 115 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= 116 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 117 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 119 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 120 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= 121 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 122 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 123 | golang.org/x/sys v0.0.0-20190203050204-7ae0202eb74c h1:YeMXU0KQqExdpG959DFhAhfpY8myIsnfqj8lhNFRzzE= 124 | golang.org/x/sys v0.0.0-20190203050204-7ae0202eb74c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 125 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 126 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= 127 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 128 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 129 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 130 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 131 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 132 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 133 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 134 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 135 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 136 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= 137 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 138 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 139 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 140 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 141 | --------------------------------------------------------------------------------