├── .github └── workflows │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── functional_test.go ├── go.mod ├── go.sum ├── hook.go └── hook_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | go-version: [ '1.17', '1.18.x', '1.19.x' ] 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Go ${{ matrix.go-version }} 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | 22 | - name: Test 23 | run: go test -v ./... 24 | -------------------------------------------------------------------------------- /.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 | .idea 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | *.iml 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0 4 | 5 | * Remove the old API: `NewConnWith`, `WithPrefix` and etc and move to a simple `New` function. 6 | * Prefix is no longer supported in this package. 7 | * Change the Hook structure to have only two members: `logrus.Formatter` and `io.Writer`. 8 | 9 | ## 0.4 10 | 11 | * Update the name of the package from `logrus_logstash` to `logrustash` 12 | * Add TimeFormat to Hook 13 | * Replace the old logrus package path: `github.com/Sirupsen/logrus` with `github.com/sirupsen/logrus` 14 | 15 | ## 0.3 16 | 17 | * Fix the Logstash format to set `@version` to `"1"` 18 | * Add unit-tests to logstash.go 19 | * Remove the assert package 20 | * Add prefix filtering 21 | 22 | ## Before that (major changes) 23 | 24 | * Update LICENSE to MIT from GPL 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Boaz Shuster 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logstash hook for logrus :walrus: 2 | [![Build Status](https://travis-ci.org/bshuster-repo/logrus-logstash-hook.svg?branch=master)](https://travis-ci.org/bshuster-repo/logrus-logstash-hook) 3 | [![Go Report Status](https://goreportcard.com/badge/github.com/bshuster-repo/logrus-logstash-hook)](https://goreportcard.com/report/github.com/bshuster-repo/logrus-logstash-hook) 4 | 5 | Use this hook to send the logs to [Logstash](https://www.elastic.co/products/logstash). 6 | 7 | # Usage 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "github.com/bshuster-repo/logrus-logstash-hook" 14 | "github.com/sirupsen/logrus" 15 | "net" 16 | ) 17 | 18 | func main() { 19 | log := logrus.New() 20 | conn, err := net.Dial("tcp", "logstash.mycompany.net:8911") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | hook := logrustash.New(conn, logrustash.DefaultFormatter(logrus.Fields{"type": "myappName"})) 25 | 26 | log.Hooks.Add(hook) 27 | ctx := log.WithFields(logrus.Fields{ 28 | "method": "main", 29 | }) 30 | ctx.Info("Hello World!") 31 | } 32 | 33 | ``` 34 | 35 | This is how it will look like: 36 | 37 | ```ruby 38 | { 39 | "@timestamp" => "2016-02-29T16:57:23.000Z", 40 | "@version" => "1", 41 | "level" => "info", 42 | "message" => "Hello World!", 43 | "method" => "main", 44 | "host" => "172.17.0.1", 45 | "port" => 45199, 46 | "type" => "myappName" 47 | } 48 | ``` 49 | 50 | # FAQ 51 | Q: I would like to add characters to each line before sending to Logstash? 52 | A: Logrustash gives you the ability to mutate the message before sending it to Logstash. Just follow [this example](https://github.com/bshuster-repo/logrus-logstash-hook/issues/60#issuecomment-604948272). 53 | 54 | Q: Is there a way to maintain the connection when it drops 55 | A: It's recommended to use [GoAutoSocket](https://github.com/firstrow/goautosocket) for that. See [here](https://github.com/bshuster-repo/logrus-logstash-hook/issues/48#issuecomment-361938249) how it can be done. 56 | 57 | # Maintainers 58 | 59 | Name | Github | 60 | ------------ | --------- | 61 | Boaz Shuster | boaz0 | 62 | 63 | # License 64 | 65 | MIT. 66 | -------------------------------------------------------------------------------- /functional_test.go: -------------------------------------------------------------------------------- 1 | package logrustash_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/bshuster-repo/logrus-logstash-hook" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func TestEntryIsNotChangedByLogstashFormatter(t *testing.T) { 16 | buffer := bytes.NewBufferString("") 17 | bufferOut := bytes.NewBufferString("") 18 | 19 | log := logrus.New() 20 | log.Out = bufferOut 21 | 22 | hook := logrustash.New(buffer, logrustash.DefaultFormatter(logrus.Fields{"NICKNAME": ""})) 23 | log.Hooks.Add(hook) 24 | 25 | log.Info("hello world") 26 | 27 | if !strings.Contains(buffer.String(), "NICKNAME\":") { 28 | t.Errorf("expected logstash message to have '%s': %#v", "NICKNAME\":", buffer.String()) 29 | } 30 | if strings.Contains(bufferOut.String(), "NICKNAME\":") { 31 | t.Errorf("expected main logrus message to not have '%s': %#v", "NICKNAME\":", buffer.String()) 32 | } 33 | } 34 | 35 | func TestTimestampFormatKitchen(t *testing.T) { 36 | log := logrus.New() 37 | buffer := bytes.NewBufferString("") 38 | hook := logrustash.New(buffer, logrustash.LogstashFormatter{ 39 | Formatter: &logrus.JSONFormatter{ 40 | FieldMap: logrus.FieldMap{ 41 | logrus.FieldKeyTime: "@timestamp", 42 | logrus.FieldKeyMsg: "message", 43 | }, 44 | TimestampFormat: time.Kitchen, 45 | }, 46 | Fields: logrus.Fields{"HOSTNAME": "localhost", "USERNAME": "root"}, 47 | }) 48 | log.Hooks.Add(hook) 49 | 50 | log.Error("this is an error message!") 51 | mTime := time.Now() 52 | expected := fmt.Sprintf(`{"@timestamp":"%s","HOSTNAME":"localhost","USERNAME":"root","level":"error","message":"this is an error message!"} 53 | `, mTime.Format(time.Kitchen)) 54 | if buffer.String() != expected { 55 | t.Errorf("expected JSON to be '%#v' but got '%#v'", expected, buffer.String()) 56 | } 57 | } 58 | 59 | func TestTextFormatLogstash(t *testing.T) { 60 | log := logrus.New() 61 | buffer := bytes.NewBufferString("") 62 | hook := logrustash.New(buffer, logrustash.LogstashFormatter{ 63 | Formatter: &logrus.TextFormatter{ 64 | TimestampFormat: time.Kitchen, 65 | }, 66 | Fields: logrus.Fields{"HOSTNAME": "localhost", "USERNAME": "root"}, 67 | }) 68 | log.Hooks.Add(hook) 69 | 70 | log.Warning("this is a warning message!") 71 | mTime := time.Now() 72 | expected := fmt.Sprintf(`time="%s" level=warning msg="this is a warning message!" HOSTNAME=localhost USERNAME=root 73 | `, mTime.Format(time.Kitchen)) 74 | if buffer.String() != expected { 75 | t.Errorf("expected JSON to be '%#v' but got '%#v'", expected, buffer.String()) 76 | } 77 | } 78 | 79 | // Github issue #39 80 | func TestLogWithFieldsDoesNotOverrideHookFields(t *testing.T) { 81 | log := logrus.New() 82 | buffer := bytes.NewBufferString("") 83 | hook := logrustash.New(buffer, logrustash.LogstashFormatter{ 84 | Formatter: &logrus.JSONFormatter{}, 85 | Fields: logrus.Fields{}, 86 | }) 87 | log.Hooks.Add(hook) 88 | log.WithField("animal", "walrus").Info("bla") 89 | attr := "\"animal\":\"walrus\"" 90 | if !strings.Contains(buffer.String(), attr) { 91 | t.Errorf("expected to have '%s' in '%s'", attr, buffer.String()) 92 | } 93 | buffer.Reset() 94 | log.Info("hahaha") 95 | if strings.Contains(buffer.String(), attr) { 96 | t.Errorf("expected not to have '%s' in '%s'", attr, buffer.String()) 97 | } 98 | } 99 | 100 | func TestDefaultFormatterNotOverrideMyLogstashFieldsValues(t *testing.T) { 101 | formatter := logrustash.DefaultFormatter(logrus.Fields{"@version": "2", "type": "mylogs"}) 102 | 103 | dataBytes, err := formatter.Format(&logrus.Entry{Data: logrus.Fields{}}) 104 | if err != nil { 105 | t.Errorf("expected Format to not return error: %s", err) 106 | } 107 | 108 | expected := []string{ 109 | `"@version":"2"`, 110 | `"type":"mylogs"`, 111 | } 112 | 113 | for _, expField := range expected { 114 | if !strings.Contains(string(dataBytes), expField) { 115 | t.Errorf("expected '%s' to be in '%s'", expField, string(dataBytes)) 116 | } 117 | } 118 | } 119 | 120 | func TestDefaultFormatterLogstashFields(t *testing.T) { 121 | formatter := logrustash.DefaultFormatter(logrus.Fields{}) 122 | 123 | dataBytes, err := formatter.Format(&logrus.Entry{Data: logrus.Fields{}}) 124 | if err != nil { 125 | t.Errorf("expected Format to not return error: %s", err) 126 | } 127 | 128 | expected := []string{ 129 | `"@version":"1"`, 130 | `"type":"log"`, 131 | } 132 | 133 | for _, expField := range expected { 134 | if !strings.Contains(string(dataBytes), expField) { 135 | t.Errorf("expected '%s' to be in '%s'", expField, string(dataBytes)) 136 | } 137 | } 138 | } 139 | 140 | // UDP will never fail because it's connectionless. 141 | // That's why I am using it for this integration tests just to make sure 142 | // it won't fail when a data is written. 143 | func TestUDPWritter(t *testing.T) { 144 | log := logrus.New() 145 | conn, err := net.Dial("udp", ":8282") 146 | if err != nil { 147 | t.Errorf("expected Dial to not return error: %s", err) 148 | } 149 | hook := logrustash.New(conn, &logrus.JSONFormatter{}) 150 | log.Hooks.Add(hook) 151 | 152 | log.Info("this is an information message") 153 | } 154 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bshuster-repo/logrus-logstash-hook 2 | 3 | go 1.16 4 | 5 | require github.com/sirupsen/logrus v1.8.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 6 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 7 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 8 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 9 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 10 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 11 | -------------------------------------------------------------------------------- /hook.go: -------------------------------------------------------------------------------- 1 | package logrustash 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sync" 7 | "time" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // Hook represents a Logstash hook. 13 | // It has two fields: writer to write the entry to Logstash and 14 | // formatter to format the entry to a Logstash format before sending. 15 | // 16 | // To initialize it use the `New` function. 17 | type Hook struct { 18 | writer io.Writer 19 | formatter logrus.Formatter 20 | } 21 | 22 | // New returns a new logrus.Hook for Logstash. 23 | // 24 | // To create a new hook that sends logs to `tcp://logstash.corp.io:9999`: 25 | // 26 | // conn, _ := net.Dial("tcp", "logstash.corp.io:9999") 27 | // hook := logrustash.New(conn, logrustash.DefaultFormatter()) 28 | func New(w io.Writer, f logrus.Formatter) logrus.Hook { 29 | return Hook{ 30 | writer: w, 31 | formatter: f, 32 | } 33 | } 34 | 35 | // Fire takes, formats and sends the entry to Logstash. 36 | // Hook's formatter is used to format the entry into Logstash format 37 | // and Hook's writer is used to write the formatted entry to the Logstash instance. 38 | func (h Hook) Fire(e *logrus.Entry) error { 39 | dataBytes, err := h.formatter.Format(e) 40 | if err != nil { 41 | return err 42 | } 43 | _, err = h.writer.Write(dataBytes) 44 | return err 45 | } 46 | 47 | // Levels returns all logrus levels. 48 | func (h Hook) Levels() []logrus.Level { 49 | return logrus.AllLevels 50 | } 51 | 52 | // Using a pool to re-use of old entries when formatting Logstash messages. 53 | // It is used in the Fire function. 54 | var entryPool = sync.Pool{ 55 | New: func() interface{} { 56 | return &logrus.Entry{} 57 | }, 58 | } 59 | 60 | // copyEntry copies the entry `e` to a new entry and then adds all the fields in `fields` that are missing in the new entry data. 61 | // It uses `entryPool` to re-use allocated entries. 62 | func copyEntry(e *logrus.Entry, fields logrus.Fields) *logrus.Entry { 63 | ne := entryPool.Get().(*logrus.Entry) 64 | ne.Message = e.Message 65 | ne.Level = e.Level 66 | ne.Time = e.Time 67 | ne.Data = logrus.Fields{} 68 | 69 | if e.HasCaller() { 70 | ne.Caller = e.Caller 71 | ne.Logger = e.Logger 72 | ne.Data["function"] = e.Caller.Function 73 | ne.Data["file"] = fmt.Sprintf("%s:%d", e.Caller.File, e.Caller.Line) 74 | } 75 | 76 | for k, v := range fields { 77 | ne.Data[k] = v 78 | } 79 | for k, v := range e.Data { 80 | ne.Data[k] = v 81 | } 82 | return ne 83 | } 84 | 85 | // releaseEntry puts the given entry back to `entryPool`. It must be called if copyEntry is called. 86 | func releaseEntry(e *logrus.Entry) { 87 | entryPool.Put(e) 88 | } 89 | 90 | // LogstashFormatter represents a Logstash format. 91 | // It has logrus.Formatter which formats the entry and logrus.Fields which 92 | // are added to the JSON message if not given in the entry data. 93 | // 94 | // Note: use the `DefaultFormatter` function to set a default Logstash formatter. 95 | type LogstashFormatter struct { 96 | logrus.Formatter 97 | logrus.Fields 98 | } 99 | 100 | var ( 101 | logstashFields = logrus.Fields{"@version": "1", "type": "log"} 102 | logstashFieldMap = logrus.FieldMap{ 103 | logrus.FieldKeyTime: "@timestamp", 104 | logrus.FieldKeyMsg: "message", 105 | } 106 | ) 107 | 108 | // DefaultFormatter returns a default Logstash formatter: 109 | // A JSON format with "@version" set to "1" (unless set differently in `fields`, 110 | // "type" to "log" (unless set differently in `fields`), 111 | // "@timestamp" to the log time and "message" to the log message. 112 | // 113 | // Note: to set a different configuration use the `LogstashFormatter` structure. 114 | func DefaultFormatter(fields logrus.Fields) logrus.Formatter { 115 | for k, v := range logstashFields { 116 | if _, ok := fields[k]; !ok { 117 | fields[k] = v 118 | } 119 | } 120 | 121 | return LogstashFormatter{ 122 | Formatter: &logrus.JSONFormatter{ 123 | FieldMap: logstashFieldMap, 124 | TimestampFormat: time.RFC3339Nano, 125 | }, 126 | Fields: fields, 127 | } 128 | } 129 | 130 | // Format formats an entry to a Logstash format according to the given Formatter and Fields. 131 | // 132 | // Note: the given entry is copied and not changed during the formatting process. 133 | func (f LogstashFormatter) Format(e *logrus.Entry) ([]byte, error) { 134 | ne := copyEntry(e, f.Fields) 135 | dataBytes, err := f.Formatter.Format(ne) 136 | releaseEntry(ne) 137 | return dataBytes, err 138 | } 139 | -------------------------------------------------------------------------------- /hook_test.go: -------------------------------------------------------------------------------- 1 | package logrustash 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type simpleFmter struct{} 15 | 16 | func (f simpleFmter) Format(e *logrus.Entry) ([]byte, error) { 17 | return []byte(fmt.Sprintf("msg: %#v", e.Message)), nil 18 | } 19 | 20 | func TestFire(t *testing.T) { 21 | buffer := bytes.NewBuffer(nil) 22 | h := Hook{ 23 | writer: buffer, 24 | formatter: simpleFmter{}, 25 | } 26 | 27 | entry := &logrus.Entry{ 28 | Message: "my message", 29 | Data: logrus.Fields{}, 30 | } 31 | 32 | err := h.Fire(entry) 33 | if err != nil { 34 | t.Error("expected Fire to not return error") 35 | } 36 | 37 | expected := "msg: \"my message\"" 38 | if buffer.String() != expected { 39 | t.Errorf("expected to see '%s' in '%s'", expected, buffer.String()) 40 | } 41 | } 42 | 43 | type FailFmt struct{} 44 | 45 | func (f FailFmt) Format(e *logrus.Entry) ([]byte, error) { 46 | return nil, errors.New("") 47 | } 48 | 49 | func TestFireFormatError(t *testing.T) { 50 | buffer := bytes.NewBuffer(nil) 51 | h := Hook{ 52 | writer: buffer, 53 | formatter: FailFmt{}, 54 | } 55 | 56 | if err := h.Fire(&logrus.Entry{Data: logrus.Fields{}}); err == nil { 57 | t.Error("expected Fire to return error") 58 | } 59 | } 60 | 61 | type FailWrite struct{} 62 | 63 | func (w FailWrite) Write(d []byte) (int, error) { 64 | return 0, errors.New("") 65 | } 66 | 67 | func TestFireWriteError(t *testing.T) { 68 | h := Hook{ 69 | writer: FailWrite{}, 70 | formatter: &logrus.JSONFormatter{}, 71 | } 72 | 73 | if err := h.Fire(&logrus.Entry{Data: logrus.Fields{}}); err == nil { 74 | t.Error("expected Fire to return error") 75 | } 76 | } 77 | 78 | func TestDefaultFormatterWithFields(t *testing.T) { 79 | format := DefaultFormatter(logrus.Fields{"ID": 123}) 80 | 81 | entry := &logrus.Entry{ 82 | Message: "msg1", 83 | Data: logrus.Fields{"f1": "bla"}, 84 | } 85 | 86 | res, err := format.Format(entry) 87 | if err != nil { 88 | t.Errorf("expected format to not return error: %s", err) 89 | } 90 | 91 | expected := []string{ 92 | "f1\":\"bla\"", 93 | "ID\":123", 94 | "message\":\"msg1\"", 95 | } 96 | 97 | for _, exp := range expected { 98 | if !strings.Contains(string(res), exp) { 99 | t.Errorf("expected to have '%s' in '%s'", exp, string(res)) 100 | } 101 | } 102 | } 103 | 104 | func TestDefaultFormatterWithEmptyFields(t *testing.T) { 105 | now := time.Now() 106 | formatter := DefaultFormatter(logrus.Fields{}) 107 | 108 | entry := &logrus.Entry{ 109 | Message: "message bla bla", 110 | Level: logrus.DebugLevel, 111 | Time: now, 112 | Data: logrus.Fields{ 113 | "Key1": "Value1", 114 | }, 115 | } 116 | 117 | res, err := formatter.Format(entry) 118 | if err != nil { 119 | t.Errorf("expected Format not to return error: %s", err) 120 | } 121 | 122 | expected := []string{ 123 | "\"message\":\"message bla bla\"", 124 | "\"level\":\"debug\"", 125 | "\"Key1\":\"Value1\"", 126 | "\"@version\":\"1\"", 127 | "\"type\":\"log\"", 128 | fmt.Sprintf("\"@timestamp\":\"%s\"", now.Format(time.RFC3339Nano)), 129 | } 130 | 131 | for _, exp := range expected { 132 | if !strings.Contains(string(res), exp) { 133 | t.Errorf("expected to have '%s' in '%s'", exp, string(res)) 134 | } 135 | } 136 | } 137 | 138 | func TestLogstashFieldsNotOverridden(t *testing.T) { 139 | _ = DefaultFormatter(logrus.Fields{"user1": "11"}) 140 | 141 | if _, ok := logstashFields["user1"]; ok { 142 | t.Errorf("expected user1 to not be in logstashFields: %#v", logstashFields) 143 | } 144 | } 145 | --------------------------------------------------------------------------------