├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── error.go
├── gelf_reader.go
├── gelf_writer.go
├── go.mod
├── go.sum
├── graylog_hook.go
└── graylog_hook_test.go
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - "stable"
4 | - "master"
5 |
6 | env:
7 | - GO111MODULE=on
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Logrus Graylog hook
2 |
3 | ## 3.0.3 - 2019-12-28
4 |
5 | * Fix concurrent logging when hook is reused (#49)
6 |
7 | ## 3.0.2 - 2019-01-10
8 |
9 | * TRACE level logs as syslog `LOG_DEBUG` level (7)
10 |
11 | ## 3.0.1 - 2019-01-09
12 |
13 | * Make pipeline green again. Credits: @psampaz
14 |
15 | ## 3.0.0 - 2019-01-08
16 |
17 | * [Use logrus ReportCaller to get file, line and function](https://github.com/gemnasium/logrus-graylog-hook/pull/39). Breaking change: This change removes the `File` and `Line` fields of entries, and replace them with `_file`, `_line`, and `method` when `ReportCaller` is true (see logrus.SetReportCaller). Credits: @psampaz
18 | * Make this package a go module
19 |
20 | ## 2.0.7 - 2018-02-09
21 |
22 | * Fix reported levels to match syslog levels (@maxatome / #27)
23 | * Removed go 1.3 support
24 |
25 | ## 2.0.6 - 2017-06-01
26 |
27 | * Update import logrus path. See https://github.com/sirupsen/logrus/pull/384
28 |
29 | ## 2.0.5 - 2017-04-14
30 |
31 | * Support uncompressed messages (@yuancheng-p / #24)
32 |
33 | ## 2.0.4 - 2017-02-19
34 |
35 | * Avoid panic if the hook can't dial Graylog (@chiffa-org / #21)
36 |
37 | ## 2.0.3 - 2016-11-30
38 |
39 | * Add support for extracting stacktraces from errors (@flimzy / #19)
40 | * Allow specifying the host instead of taking `os.Hostname` by default (@mweibel / #18)
41 |
42 | ## 2.0.2 - 2016-09-28
43 |
44 | * Get rid of github.com/SocialCodeInc/go-gelf/gelf (#14)
45 |
46 | ## 2.0.1 - 2016-08-16
47 |
48 | * Fix an issue with entry constructor (#12)
49 |
50 | ## 2.0.0 - 2016-07-02
51 |
52 | * Remove facility param in constructor, as it's an optional param in Graylog 2.0 (credits: @saward / #9)
53 | * Improve precision of TimeUnix (credits: @RaphYot / #2)
54 | * Expose Gelf Writer (we will make this an interface in later versions) (credits: @cha-won / #10)
55 |
56 | ## 1.1.2 - 2016-06-03
57 |
58 | * Fix another race condition (credits: @dreyinger / #8)
59 |
60 | ## 1.1.1 - 2016-05-10
61 |
62 | * Fix race condition (credits: @rschmukler / #6)
63 |
64 | ## 1.1.0 - 2015-12-04
65 |
66 | * The default behavior is now to send the logs synchronously.
67 | * A new asynchronous hook is available through `NewAsyncGraylogHook`
68 |
69 |
70 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Gemnasium
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Graylog Hook for [Logrus](https://github.com/sirupsen/logrus)
[](https://travis-ci.org/gemnasium/logrus-graylog-hook) [](https://godoc.org/github.com/gemnasium/logrus-graylog-hook)
2 |
3 | Use this hook to send your logs to [Graylog](http://graylog2.org) server over UDP.
4 | The hook is non-blocking: even if UDP is used to send messages, the extra work
5 | should not block the logging function.
6 |
7 | All logrus fields will be sent as additional fields on Graylog.
8 |
9 | ## Usage
10 |
11 | The hook must be configured with:
12 |
13 | * A Graylog GELF UDP address (a "ip:port" string).
14 | * an optional hash with extra global fields. These fields will be included in all messages sent to Graylog
15 |
16 | ```go
17 | package main
18 |
19 | import (
20 | log "github.com/sirupsen/logrus"
21 | "github.com/gemnasium/logrus-graylog-hook/v3"
22 | )
23 |
24 | func main() {
25 | hook := graylog.NewGraylogHook(":", map[string]interface{}{"this": "is logged every time"})
26 | log.AddHook(hook)
27 | log.Info("some logging message")
28 | }
29 | ```
30 |
31 | ### Asynchronous logger
32 |
33 | ```go
34 | package main
35 |
36 | import (
37 | log "github.com/sirupsen/logrus"
38 | "github.com/gemnasium/logrus-graylog-hook/v3"
39 | )
40 |
41 | func main() {
42 | hook := graylog.NewAsyncGraylogHook(":", map[string]interface{}{"this": "is logged every time"})
43 | // NOTE: you must call Flush() before your program exits to ensure ALL of your logs are sent.
44 | // This defer statement will not have that effect if you write it in a non-main() method.
45 | defer hook.Flush()
46 | log.AddHook(hook)
47 | log.Info("some logging message")
48 | }
49 | ```
50 |
51 | ### Disable standard logging
52 |
53 | For some reason, you may want to disable logging on stdout, and keep only the messages in Graylog (ie: a webserver inside a docker container).
54 | You can redirect `stdout` to `/dev/null`, or just not log anything by creating a `NullFormatter` implementing `logrus.Formatter` interface:
55 |
56 | ```go
57 | type NullFormatter struct {
58 | }
59 |
60 | // Don't spend time formatting logs
61 | func (NullFormatter) Format(e *log.Entry) ([]byte, error) {
62 | return []byte{}, nil
63 | }
64 | ```
65 |
66 | And set this formatter as the new logging formatter:
67 |
68 | ```go
69 | log.Infof("Log messages are now sent to Graylog (udp://%s)", graylogAddr) // Give a hint why logs are empty
70 | log.AddHook(graylog.NewGraylogHook(graylogAddr, "api", map[string]interface{}{})) // set graylogAddr accordingly
71 | log.SetFormatter(new(NullFormatter)) // Don't send logs to stdout
72 | ```
73 |
--------------------------------------------------------------------------------
/error.go:
--------------------------------------------------------------------------------
1 | package graylog
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/pkg/errors"
6 | )
7 |
8 | // newMarshalableError builds an error which encodes its error message into JSON
9 | func newMarshalableError(err error) *marshalableError {
10 | return &marshalableError{err}
11 | }
12 |
13 | // a marshalableError is an error that can be encoded into JSON
14 | type marshalableError struct {
15 | err error
16 | }
17 |
18 | // MarshalJSON implements json.Marshaler for marshalableError
19 | func (m *marshalableError) MarshalJSON() ([]byte, error) {
20 | return json.Marshal(m.err.Error())
21 | }
22 |
23 | type causer interface {
24 | Cause() error
25 | }
26 |
27 | type stackTracer interface {
28 | StackTrace() errors.StackTrace
29 | }
30 |
31 | func extractStackTrace(err error) errors.StackTrace {
32 | var tracer stackTracer
33 | for {
34 | if st, ok := err.(stackTracer); ok {
35 | tracer = st
36 | }
37 | if cause, ok := err.(causer); ok {
38 | err = cause.Cause()
39 | continue
40 | }
41 | break
42 | }
43 | if tracer == nil {
44 | return nil
45 | }
46 | return tracer.StackTrace()
47 | }
48 |
--------------------------------------------------------------------------------
/gelf_reader.go:
--------------------------------------------------------------------------------
1 | package graylog
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "compress/zlib"
7 | "encoding/json"
8 | "fmt"
9 | "io"
10 | "net"
11 | "strings"
12 | "sync"
13 | )
14 |
15 | type Reader struct {
16 | mu sync.Mutex
17 | conn net.Conn
18 | }
19 |
20 | func NewUDPReader(addr string) (*Reader, error) {
21 | var err error
22 | udpAddr, err := net.ResolveUDPAddr("udp", addr)
23 | if err != nil {
24 | return nil, fmt.Errorf("ResolveUDPAddr('%s'): %s", addr, err)
25 | }
26 |
27 | conn, err := net.ListenUDP("udp", udpAddr)
28 | if err != nil {
29 | return nil, fmt.Errorf("ListenUDP: %s", err)
30 | }
31 |
32 | r := new(Reader)
33 | r.conn = conn
34 | return r, nil
35 | }
36 |
37 | func NewTCPReader(addr string) (*net.TCPListener, error) {
38 | var err error
39 | tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
40 | if err != nil {
41 | return nil, fmt.Errorf("ResolveTCPAddr('%s'): %s", addr, err)
42 | }
43 |
44 | listener, err := net.ListenTCP("tcp", tcpAddr)
45 | if err != nil {
46 | return nil, fmt.Errorf("ListenTCP: %s", err)
47 | }
48 |
49 | return listener, nil
50 | }
51 |
52 | func (r *Reader) Addr() string {
53 | return r.conn.LocalAddr().String()
54 | }
55 |
56 | // FIXME: this will discard data if p isn't big enough to hold the
57 | // full message.
58 | func (r *Reader) Read(p []byte) (int, error) {
59 | msg, err := r.ReadMessage()
60 | if err != nil {
61 | return -1, err
62 | }
63 |
64 | var data string
65 |
66 | if msg.Full == "" {
67 | data = msg.Short
68 | } else {
69 | data = msg.Full
70 | }
71 |
72 | return strings.NewReader(data).Read(p)
73 | }
74 |
75 | func (r *Reader) ReadMessage() (*Message, error) {
76 | cBuf := make([]byte, ChunkSize)
77 | var (
78 | err error
79 | n, length int
80 | buf bytes.Buffer
81 | cid, ocid []byte
82 | seq, total uint8
83 | cHead []byte
84 | cReader io.Reader
85 | chunks [][]byte
86 | )
87 |
88 | for got := 0; got < 128 && (total == 0 || got < int(total)); got++ {
89 | if n, err = r.conn.Read(cBuf); err != nil {
90 | return nil, fmt.Errorf("Read: %s", err)
91 | }
92 | cHead, cBuf = cBuf[:2], cBuf[:n]
93 |
94 | if bytes.Equal(cHead, magicChunked) {
95 | //fmt.Printf("chunked %v\n", cBuf[:14])
96 | cid, seq, total = cBuf[2:2+8], cBuf[2+8], cBuf[2+8+1]
97 | if ocid != nil && !bytes.Equal(cid, ocid) {
98 | return nil, fmt.Errorf("out-of-band message %v (awaited %v)", cid, ocid)
99 | } else if ocid == nil {
100 | ocid = cid
101 | chunks = make([][]byte, total)
102 | }
103 | n = len(cBuf) - chunkedHeaderLen
104 | //fmt.Printf("setting chunks[%d]: %d\n", seq, n)
105 | chunks[seq] = append(make([]byte, 0, n), cBuf[chunkedHeaderLen:]...)
106 | length += n
107 | } else { //not chunked
108 | if total > 0 {
109 | return nil, fmt.Errorf("out-of-band message (not chunked)")
110 | }
111 | break
112 | }
113 | }
114 | //fmt.Printf("\nchunks: %v\n", chunks)
115 |
116 | if length > 0 {
117 | if cap(cBuf) < length {
118 | cBuf = append(cBuf, make([]byte, 0, length-cap(cBuf))...)
119 | }
120 | cBuf = cBuf[:0]
121 | for i := range chunks {
122 | //fmt.Printf("appending %d %v\n", i, chunks[i])
123 | cBuf = append(cBuf, chunks[i]...)
124 | }
125 | cHead = cBuf[:2]
126 | }
127 |
128 | // the data we get from the wire is compressed
129 | if bytes.Equal(cHead, magicGzip) {
130 | cReader, err = gzip.NewReader(bytes.NewReader(cBuf))
131 | } else if cHead[0] == magicZlib[0] &&
132 | (int(cHead[0])*256+int(cHead[1]))%31 == 0 {
133 | // zlib is slightly more complicated, but correct
134 | cReader, err = zlib.NewReader(bytes.NewReader(cBuf))
135 | } else {
136 | return nil, fmt.Errorf("unknown magic: %x %v", cHead, cHead)
137 | }
138 |
139 | if err != nil {
140 | return nil, fmt.Errorf("NewUDPReader: %s", err)
141 | }
142 |
143 | if _, err = io.Copy(&buf, cReader); err != nil {
144 | return nil, fmt.Errorf("io.Copy: %s", err)
145 | }
146 |
147 | msg := new(Message)
148 | if err := json.Unmarshal(buf.Bytes(), &msg); err != nil {
149 | return nil, fmt.Errorf("json.Unmarshal: %s", err)
150 | }
151 |
152 | return msg, nil
153 | }
154 |
--------------------------------------------------------------------------------
/gelf_writer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2012 SocialCode. All rights reserved.
2 | // Use of this source code is governed by the MIT
3 | // license that can be found in the LICENSE file.
4 |
5 | package graylog
6 |
7 | import (
8 | "bytes"
9 | "compress/flate"
10 | "compress/gzip"
11 | "compress/zlib"
12 | "crypto/rand"
13 | "encoding/json"
14 | "fmt"
15 | "io"
16 | "net"
17 | "net/http"
18 | "os"
19 | "path"
20 | "strings"
21 | "sync"
22 | "time"
23 | )
24 |
25 | type GELFWriter interface {
26 | WriteMessage(m *Message) (err error)
27 | }
28 |
29 | // LowLevelProtocolWriter implements io.Writer and is used to send both discrete
30 | // messages to a graylog2 server, or data from a stream-oriented
31 | // interface (like the functions in log).
32 | type LowLevelProtocolWriter struct {
33 | mu sync.Mutex
34 | conn net.Conn
35 | hostname string
36 | Facility string // defaults to current process name
37 | CompressionLevel int // one of the consts from compress/flate
38 | CompressionType CompressType
39 |
40 | zw writerCloserResetter
41 | zwCompressionLevel int
42 | zwCompressionType CompressType
43 | }
44 |
45 | // What compression type the writer should use when sending messages
46 | // to the graylog2 server
47 | type CompressType int
48 |
49 | const (
50 | CompressGzip CompressType = iota
51 | CompressZlib
52 | NoCompress
53 | )
54 |
55 | // Message represents the contents of the GELF message. It is gzipped
56 | // before sending.
57 | type Message struct {
58 | Version string `json:"version"`
59 | Host string `json:"host"`
60 | Short string `json:"short_message"`
61 | Full string `json:"full_message"`
62 | TimeUnix float64 `json:"timestamp"`
63 | Level int32 `json:"level"`
64 | Facility string `json:"facility"`
65 | File string `json:"file"`
66 | Line int `json:"line"`
67 | Extra map[string]interface{} `json:"-"`
68 | }
69 |
70 | type innerMessage Message //against circular (Un)MarshalJSON
71 |
72 | // Used to control GELF chunking. Should be less than (MTU - len(UDP
73 | // header)).
74 | //
75 | // TODO: generate dynamically using Path MTU Discovery?
76 | const (
77 | ChunkSize = 1420
78 | chunkedHeaderLen = 12
79 | chunkedDataLen = ChunkSize - chunkedHeaderLen
80 | )
81 |
82 | var (
83 | magicChunked = []byte{0x1e, 0x0f}
84 | magicZlib = []byte{0x78}
85 | magicGzip = []byte{0x1f, 0x8b}
86 | )
87 |
88 | // numChunks returns the number of GELF chunks necessary to transmit
89 | // the given compressed buffer.
90 | func numChunks(b []byte) int {
91 | lenB := len(b)
92 | if lenB <= ChunkSize {
93 | return 1
94 | }
95 | return len(b)/chunkedDataLen + 1
96 | }
97 |
98 | // NewWriter returns a new GELFWriter. This writer can be used to send the
99 | // output of the standard Go log functions to a central GELF server by
100 | // passing it to log.SetOutput()
101 | func NewWriter(addr string) (GELFWriter, error) {
102 | if strings.HasPrefix(addr, "http") {
103 | return newHTTPWriter(addr)
104 | }
105 | if strings.HasPrefix(addr, "tcp://") {
106 | return newLowLevelProtocolWriter("tcp", strings.TrimPrefix(addr, "tcp://"))
107 | }
108 |
109 | return newLowLevelProtocolWriter("udp", addr)
110 | }
111 |
112 | func newHTTPWriter(addr string) (GELFWriter, error) {
113 | httpClient := &http.Client{
114 | Transport: &http.Transport{},
115 | Timeout: 10 * time.Second,
116 | }
117 |
118 | return HTTPWriter{
119 | httpClient: httpClient,
120 | addr: addr,
121 | }, nil
122 | }
123 |
124 | func newLowLevelProtocolWriter(protocol, addr string) (GELFWriter, error) {
125 | var err error
126 | w := new(LowLevelProtocolWriter)
127 | w.CompressionLevel = flate.BestSpeed
128 |
129 | if w.conn, err = net.Dial(protocol, strings.TrimPrefix(addr, "tcp://")); err != nil {
130 | return nil, err
131 | }
132 |
133 | if w.hostname, err = os.Hostname(); err != nil {
134 | return nil, err
135 | }
136 |
137 | w.Facility = path.Base(os.Args[0])
138 |
139 | return w, nil
140 | }
141 |
142 | // writes the gzip compressed byte array to the connection as a series
143 | // of GELF chunked messages. The header format is documented at
144 | // https://github.com/Graylog2/graylog2-docs/wiki/GELF as:
145 | //
146 | // 2-byte magic (0x1e 0x0f), 8 byte id, 1 byte sequence id, 1 byte
147 | // total, chunk-data
148 | func (w *LowLevelProtocolWriter) writeChunked(zBytes []byte) (err error) {
149 | b := make([]byte, 0, ChunkSize)
150 | buf := bytes.NewBuffer(b)
151 | nChunksI := numChunks(zBytes)
152 | if nChunksI > 255 {
153 | return fmt.Errorf("msg too large, would need %d chunks", nChunksI)
154 | }
155 | nChunks := uint8(nChunksI)
156 | // use urandom to get a unique message id
157 | msgId := make([]byte, 8)
158 | n, err := io.ReadFull(rand.Reader, msgId)
159 | if err != nil || n != 8 {
160 | return fmt.Errorf("rand.Reader: %d/%s", n, err)
161 | }
162 |
163 | bytesLeft := len(zBytes)
164 | for i := uint8(0); i < nChunks; i++ {
165 | buf.Reset()
166 | // manually write header. Don't care about
167 | // host/network byte order, because the spec only
168 | // deals in individual bytes.
169 | buf.Write(magicChunked) //magic
170 | buf.Write(msgId)
171 | buf.WriteByte(i)
172 | buf.WriteByte(nChunks)
173 | // slice out our chunk from zBytes
174 | chunkLen := chunkedDataLen
175 | if chunkLen > bytesLeft {
176 | chunkLen = bytesLeft
177 | }
178 | off := int(i) * chunkedDataLen
179 | chunk := zBytes[off : off+chunkLen]
180 | buf.Write(chunk)
181 |
182 | // write this chunk, and make sure the write was good
183 | n, err := w.conn.Write(buf.Bytes())
184 | if err != nil {
185 | return fmt.Errorf("Write (chunk %d/%d): %s", i,
186 | nChunks, err)
187 | }
188 | if n != len(buf.Bytes()) {
189 | return fmt.Errorf("Write len: (chunk %d/%d) (%d/%d)",
190 | i, nChunks, n, len(buf.Bytes()))
191 | }
192 |
193 | bytesLeft -= chunkLen
194 | }
195 |
196 | if bytesLeft != 0 {
197 | return fmt.Errorf("error: %d bytes left after sending", bytesLeft)
198 | }
199 | return nil
200 | }
201 |
202 | type bufferedWriter struct {
203 | buffer io.Writer
204 | }
205 |
206 | func (bw bufferedWriter) Write(p []byte) (n int, err error) {
207 | return bw.buffer.Write(p)
208 | }
209 |
210 | func (bw bufferedWriter) Close() error {
211 | return nil
212 | }
213 |
214 | func (bw *bufferedWriter) Reset(w io.Writer) {
215 | bw.buffer = w
216 | }
217 |
218 | type writerCloserResetter interface {
219 | io.WriteCloser
220 | Reset(w io.Writer)
221 | }
222 |
223 | // WriteMessage sends the specified message to the GELF server
224 | // specified in the call to NewWriter(). It assumes all the fields are
225 | // filled out appropriately. In general, clients will want to use
226 | // Write, rather than WriteMessage.
227 | func (w *LowLevelProtocolWriter) WriteMessage(m *Message) (err error) {
228 | w.mu.Lock()
229 | defer w.mu.Unlock()
230 |
231 | mBytes, err := json.Marshal(m)
232 | if err != nil {
233 | return
234 | }
235 |
236 | var zBuf bytes.Buffer
237 |
238 | // . If compression settings have changed, a new writer is required.
239 | if w.zwCompressionType != w.CompressionType || w.zwCompressionLevel != w.CompressionLevel {
240 | w.zw = nil
241 | }
242 |
243 | switch w.CompressionType {
244 | case CompressGzip:
245 | if w.zw == nil {
246 | w.zw, err = gzip.NewWriterLevel(&zBuf, w.CompressionLevel)
247 | }
248 | case CompressZlib:
249 | if w.zw == nil {
250 | w.zw, err = zlib.NewWriterLevel(&zBuf, w.CompressionLevel)
251 | }
252 | case NoCompress:
253 | w.zw = &bufferedWriter{}
254 | default:
255 | panic(fmt.Sprintf("unknown compression type %d",
256 | w.CompressionType))
257 | }
258 |
259 | if err != nil {
260 | return
261 | }
262 |
263 | w.zw.Reset(&zBuf)
264 |
265 | if _, err = w.zw.Write(mBytes); err != nil {
266 | return
267 | }
268 | w.zw.Close()
269 |
270 | zBytes := zBuf.Bytes()
271 | if numChunks(zBytes) > 1 {
272 | return w.writeChunked(zBytes)
273 | }
274 |
275 | n, err := w.conn.Write(zBytes)
276 | if err != nil {
277 | return
278 | }
279 | if n != len(zBytes) {
280 | return fmt.Errorf("bad write (%d/%d)", n, len(zBytes))
281 | }
282 |
283 | return nil
284 | }
285 |
286 | /*
287 | func (w *Writer) Alert(m string) (err error)
288 | func (w *Writer) Close() error
289 | func (w *Writer) Crit(m string) (err error)
290 | func (w *Writer) Debug(m string) (err error)
291 | func (w *Writer) Emerg(m string) (err error)
292 | func (w *Writer) Err(m string) (err error)
293 | func (w *Writer) Info(m string) (err error)
294 | func (w *Writer) Notice(m string) (err error)
295 | func (w *Writer) Warning(m string) (err error)
296 | */
297 |
298 | // Write encodes the given string in a GELF message and sends it to
299 | // the server specified in NewWriter().
300 | func (w *LowLevelProtocolWriter) Write(p []byte) (n int, err error) {
301 |
302 | // remove trailing and leading whitespace
303 | p = bytes.TrimSpace(p)
304 |
305 | // If there are newlines in the message, use the first line
306 | // for the short message and set the full message to the
307 | // original input. If the input has no newlines, stick the
308 | // whole thing in Short.
309 | short := p
310 | full := []byte("")
311 | if i := bytes.IndexRune(p, '\n'); i > 0 {
312 | short = p[:i]
313 | full = p
314 | }
315 |
316 | m := Message{
317 | Version: "1.0",
318 | Host: w.hostname,
319 | Short: string(short),
320 | Full: string(full),
321 | TimeUnix: float64(time.Now().UnixNano()/1000000) / 1000.,
322 | Level: 6, // info
323 | Facility: w.Facility,
324 | Extra: map[string]interface{}{},
325 | }
326 |
327 | if err = w.WriteMessage(&m); err != nil {
328 | return 0, err
329 | }
330 |
331 | return len(p), nil
332 | }
333 |
334 | func (m *Message) MarshalJSON() ([]byte, error) {
335 | var err error
336 | var b, eb []byte
337 |
338 | extra := m.Extra
339 | b, err = json.Marshal((*innerMessage)(m))
340 | m.Extra = extra
341 | if err != nil {
342 | return nil, err
343 | }
344 |
345 | if len(extra) == 0 {
346 | return b, nil
347 | }
348 |
349 | if eb, err = json.Marshal(extra); err != nil {
350 | return nil, err
351 | }
352 |
353 | // merge serialized message + serialized extra map
354 | b[len(b)-1] = ','
355 | return append(b, eb[1:len(eb)]...), nil
356 | }
357 |
358 | func (m *Message) UnmarshalJSON(data []byte) error {
359 | i := make(map[string]interface{}, 16)
360 | if err := json.Unmarshal(data, &i); err != nil {
361 | return err
362 | }
363 | for k, v := range i {
364 | if k[0] == '_' {
365 | if m.Extra == nil {
366 | m.Extra = make(map[string]interface{}, 1)
367 | }
368 | m.Extra[k] = v
369 | continue
370 | }
371 | switch k {
372 | case "version":
373 | m.Version = v.(string)
374 | case "host":
375 | m.Host = v.(string)
376 | case "short_message":
377 | m.Short = v.(string)
378 | case "full_message":
379 | m.Full = v.(string)
380 | case "timestamp":
381 | m.TimeUnix = v.(float64)
382 | case "level":
383 | m.Level = int32(v.(float64))
384 | case "facility":
385 | m.Facility = v.(string)
386 | case "file":
387 | m.File = v.(string)
388 | case "line":
389 | m.Line = int(v.(float64))
390 | }
391 | }
392 | return nil
393 | }
394 |
395 | // HTTPWriter implements the GELFWriter interface, and cannot be used
396 | // as an io.Writer
397 | type HTTPWriter struct {
398 | httpClient *http.Client
399 | addr string
400 | }
401 |
402 | func (h HTTPWriter) WriteMessage(m *Message) (err error) {
403 | mBytes, err := json.Marshal(m)
404 | if err != nil {
405 | return
406 | }
407 |
408 | resp, err := h.httpClient.Post(h.addr, "application/json", bytes.NewBuffer(mBytes))
409 | if err != nil {
410 | return err
411 | }
412 | defer resp.Body.Close()
413 |
414 | if resp.StatusCode != 202 {
415 | return fmt.Errorf("got code %s, expected 202", resp.Status)
416 | }
417 |
418 | return nil
419 | }
420 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/gemnasium/logrus-graylog-hook/v3
2 |
3 | require (
4 | github.com/pkg/errors v0.9.1
5 | github.com/sirupsen/logrus v1.9.3
6 | )
7 |
8 | require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
9 |
10 | go 1.20
11 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
5 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
9 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
11 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
12 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
13 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
14 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
16 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
18 |
--------------------------------------------------------------------------------
/graylog_hook.go:
--------------------------------------------------------------------------------
1 | package graylog
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "os"
9 | "sync"
10 | "time"
11 |
12 | "github.com/sirupsen/logrus"
13 | )
14 |
15 | const StackTraceKey = "_stacktrace"
16 |
17 | // Set graylog.BufSize = _before_ calling NewGraylogHook
18 | // Once the buffer is full, logging will start blocking, waiting for slots to
19 | // be available in the queue.
20 | var BufSize uint = 8192
21 |
22 | // GraylogHook to send logs to a logging service compatible with the Graylog API and the GELF format.
23 | type GraylogHook struct {
24 | Extra map[string]interface{}
25 | Host string
26 | Level logrus.Level
27 | gelfLogger GELFWriter
28 | buf chan graylogEntry
29 | wg sync.WaitGroup
30 | mu sync.RWMutex
31 | synchronous bool
32 | blacklist map[string]bool
33 | }
34 |
35 | // Graylog needs file and line params
36 | type graylogEntry struct {
37 | *logrus.Entry
38 | file string
39 | line int
40 | }
41 |
42 | // NewGraylogHook creates a hook to be added to an instance of logger.
43 | func NewGraylogHook(addr string, extra map[string]interface{}) *GraylogHook {
44 | g, err := NewWriter(addr)
45 | if err != nil {
46 | logrus.WithError(err).Error("Can't create Gelf logger")
47 | }
48 |
49 | host, err := os.Hostname()
50 | if err != nil {
51 | host = "localhost"
52 | }
53 |
54 | hook := &GraylogHook{
55 | Host: host,
56 | Extra: extra,
57 | Level: logrus.DebugLevel,
58 | gelfLogger: g,
59 | synchronous: true,
60 | }
61 |
62 | return hook
63 | }
64 |
65 | // NewAsyncGraylogHook creates a hook to be added to an instance of logger.
66 | // The hook created will be asynchronous, and it's the responsibility of the user to call the Flush method
67 | // before exiting to empty the log queue.
68 | func NewAsyncGraylogHook(addr string, extra map[string]interface{}) *GraylogHook {
69 | g, err := NewWriter(addr)
70 | if err != nil {
71 | logrus.WithError(err).Error("Can't create Gelf logger")
72 | }
73 |
74 | host, err := os.Hostname()
75 | if err != nil {
76 | host = "localhost"
77 | }
78 |
79 | hook := &GraylogHook{
80 | Host: host,
81 | Extra: extra,
82 | Level: logrus.DebugLevel,
83 | gelfLogger: g,
84 | buf: make(chan graylogEntry, BufSize),
85 | }
86 | go hook.fire() // Log in background
87 |
88 | return hook
89 | }
90 |
91 | // Fire is called when a log event is fired.
92 | // We assume the entry will be altered by another hook,
93 | // otherwise we might be logging something wrong to Graylog
94 | func (hook *GraylogHook) Fire(entry *logrus.Entry) error {
95 | hook.mu.RLock() // Claim the mutex as a RLock - allowing multiple go routines to log simultaneously
96 | defer hook.mu.RUnlock()
97 |
98 | var file string
99 | var line int
100 |
101 | if entry.Caller != nil {
102 | file = entry.Caller.File
103 | line = entry.Caller.Line
104 | }
105 |
106 | newData := make(map[string]interface{})
107 | for k, v := range entry.Data {
108 | switch v := v.(type) {
109 | case error:
110 | // Otherwise errors are ignored by `encoding/json`
111 | // https://github.com/Sirupsen/logrus/issues/137
112 | newData[k] = v.Error()
113 | default:
114 | newData[k] = v
115 | }
116 | }
117 |
118 | newEntry := &logrus.Entry{
119 | Logger: entry.Logger,
120 | Data: newData,
121 | Time: entry.Time,
122 | Level: entry.Level,
123 | Caller: entry.Caller,
124 | Message: entry.Message,
125 | }
126 | gEntry := graylogEntry{newEntry, file, line}
127 |
128 | if hook.synchronous {
129 | hook.sendEntry(gEntry)
130 | } else {
131 | hook.wg.Add(1)
132 | hook.buf <- gEntry
133 | }
134 |
135 | return nil
136 | }
137 |
138 | // Flush waits for the log queue to be empty.
139 | // This func is meant to be used when the hook was created with NewAsyncGraylogHook.
140 | func (hook *GraylogHook) Flush() {
141 | hook.mu.Lock() // claim the mutex as a Lock - we want exclusive access to it
142 | defer hook.mu.Unlock()
143 |
144 | hook.wg.Wait()
145 | }
146 |
147 | // fire will loop on the 'buf' channel, and write entries to graylog
148 | func (hook *GraylogHook) fire() {
149 | for {
150 | entry := <-hook.buf // receive new entry on channel
151 | hook.sendEntry(entry)
152 | hook.wg.Done()
153 | }
154 | }
155 |
156 | func logrusLevelToSyslog(level logrus.Level) int32 {
157 | const (
158 | LOG_EMERG = 0 /* system is unusable */
159 | LOG_ALERT = 1 /* action must be taken immediately */
160 | LOG_CRIT = 2 /* critical conditions */
161 | LOG_ERR = 3 /* error conditions */
162 | LOG_WARNING = 4 /* warning conditions */
163 | LOG_NOTICE = 5 /* normal but significant condition */
164 | LOG_INFO = 6 /* informational */
165 | LOG_DEBUG = 7 /* debug-level messages */
166 | )
167 | // logrus has no equivalent of syslog LOG_NOTICE
168 | switch level {
169 | case logrus.PanicLevel:
170 | return LOG_ALERT
171 | case logrus.FatalLevel:
172 | return LOG_CRIT
173 | case logrus.ErrorLevel:
174 | return LOG_ERR
175 | case logrus.WarnLevel:
176 | return LOG_WARNING
177 | case logrus.InfoLevel:
178 | return LOG_INFO
179 | case logrus.DebugLevel, logrus.TraceLevel:
180 | return LOG_DEBUG
181 | default:
182 | return LOG_DEBUG
183 | }
184 | }
185 |
186 | // sendEntry sends an entry to graylog synchronously
187 | func (hook *GraylogHook) sendEntry(entry graylogEntry) {
188 | if hook.gelfLogger == nil {
189 | fmt.Println("Can't connect to Graylog")
190 | return
191 | }
192 | w := hook.gelfLogger
193 |
194 | // remove trailing and leading whitespace
195 | p := bytes.TrimSpace([]byte(entry.Message))
196 |
197 | // If there are newlines in the message, use the first line
198 | // for the short message and set the full message to the
199 | // original input. If the input has no newlines, stick the
200 | // whole thing in Short.
201 | short := p
202 | full := []byte("")
203 | if i := bytes.IndexRune(p, '\n'); i > 0 {
204 | short = p[:i]
205 | full = p
206 | }
207 |
208 | level := logrusLevelToSyslog(entry.Level)
209 |
210 | // Don't modify entry.Data directly, as the entry will used after this hook was fired
211 | extra := map[string]interface{}{}
212 | // Merge extra fields
213 | for k, v := range hook.Extra {
214 | k = fmt.Sprintf("_%s", k) // "[...] every field you send and prefix with a _ (underscore) will be treated as an additional field."
215 | extra[k] = v
216 | }
217 |
218 | if entry.Caller != nil {
219 | extra["_file"] = entry.Caller.File
220 | extra["_line"] = entry.Caller.Line
221 | extra["_function"] = entry.Caller.Function
222 | }
223 |
224 | for k, v := range entry.Data {
225 | if !hook.blacklist[k] {
226 | extraK := fmt.Sprintf("_%s", k) // "[...] every field you send and prefix with a _ (underscore) will be treated as an additional field."
227 | if k == logrus.ErrorKey {
228 | asError, isError := v.(error)
229 | _, isMarshaler := v.(json.Marshaler)
230 | if isError && !isMarshaler {
231 | extra[extraK] = newMarshalableError(asError)
232 | } else {
233 | extra[extraK] = v
234 | }
235 | if stackTrace := extractStackTrace(asError); stackTrace != nil {
236 | extra[StackTraceKey] = fmt.Sprintf("%+v", stackTrace)
237 | }
238 | } else {
239 | extra[extraK] = v
240 | }
241 | }
242 | }
243 |
244 | m := Message{
245 | Version: "1.1",
246 | Host: hook.Host,
247 | Short: string(short),
248 | Full: string(full),
249 | TimeUnix: float64(time.Now().UnixNano()/1000000) / 1000.,
250 | Level: level,
251 | File: entry.file,
252 | Line: entry.line,
253 | Extra: extra,
254 | }
255 |
256 | if err := w.WriteMessage(&m); err != nil {
257 | fmt.Println(err)
258 | }
259 | }
260 |
261 | // Levels returns the available logging levels.
262 | func (hook *GraylogHook) Levels() []logrus.Level {
263 | levels := []logrus.Level{}
264 | for _, level := range logrus.AllLevels {
265 | if level <= hook.Level {
266 | levels = append(levels, level)
267 | }
268 | }
269 | return levels
270 | }
271 |
272 | // Blacklist create a blacklist map to filter some message keys.
273 | // This useful when you want your application to log extra fields locally
274 | // but don't want graylog to store them.
275 | func (hook *GraylogHook) Blacklist(b []string) {
276 | hook.blacklist = make(map[string]bool)
277 | for _, elem := range b {
278 | hook.blacklist[elem] = true
279 | }
280 | }
281 |
282 | // SetWriter sets the hook Gelf writer
283 | func (hook *GraylogHook) SetWriter(w *LowLevelProtocolWriter) error {
284 | if w == nil {
285 | return errors.New("writer can't be nil")
286 | }
287 | hook.gelfLogger = w
288 | return nil
289 | }
290 |
291 | // Writer returns the writer
292 | func (hook *GraylogHook) Writer() GELFWriter {
293 | return hook.gelfLogger
294 | }
295 |
--------------------------------------------------------------------------------
/graylog_hook_test.go:
--------------------------------------------------------------------------------
1 | package graylog
2 |
3 | import (
4 | "compress/flate"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "io/ioutil"
10 | "net"
11 | "net/http"
12 | "net/http/httptest"
13 | "regexp"
14 | "runtime"
15 | "strings"
16 | "sync"
17 | "testing"
18 |
19 | pkgerrors "github.com/pkg/errors"
20 | "github.com/sirupsen/logrus"
21 | )
22 |
23 | const SyslogInfoLevel = 6
24 | const SyslogErrorLevel = 3
25 |
26 | func TestWritingToUDP(t *testing.T) {
27 | r, err := NewUDPReader("127.0.0.1:0")
28 | if err != nil {
29 | t.Fatalf("NewUDPReader: %s", err)
30 | }
31 | hook := NewGraylogHook(r.Addr(), map[string]interface{}{"foo": "bar"})
32 | hook.Host = "testing.local"
33 | hook.Blacklist([]string{"filterMe"})
34 | msgData := "test message\nsecond line"
35 |
36 | log := logrus.New()
37 | log.Out = io.Discard
38 | log.Hooks.Add(hook)
39 | log.WithFields(logrus.Fields{"withField": "1", "filterMe": "1"}).Info(msgData)
40 |
41 | msg, err := r.ReadMessage()
42 |
43 | if err != nil {
44 | t.Errorf("ReadMessage: %s", err)
45 | }
46 |
47 | if msg.Short != "test message" {
48 | t.Errorf("msg.Short: expected %s, got %s", msgData, msg.Full)
49 | }
50 |
51 | if msg.Full != msgData {
52 | t.Errorf("msg.Full: expected %s, got %s", msgData, msg.Full)
53 | }
54 |
55 | if msg.Level != SyslogInfoLevel {
56 | t.Errorf("msg.Level: expected: %d, got %d)", SyslogInfoLevel, msg.Level)
57 | }
58 |
59 | if msg.Host != "testing.local" {
60 | t.Errorf("Host should match (exp: testing.local, got: %s)", msg.Host)
61 | }
62 |
63 | if len(msg.Extra) != 2 {
64 | t.Errorf("wrong number of extra fields (exp: %d, got %d) in %v", 5, len(msg.Extra), msg.Extra)
65 | }
66 |
67 | fileExpected := ""
68 | if msg.File != fileExpected {
69 | t.Errorf("msg.File: expected %s, got %s", fileExpected,
70 | msg.File)
71 | }
72 |
73 | lineExpected := 0
74 | if msg.Line != lineExpected {
75 | t.Errorf("msg.Line: expected %d, got %d", lineExpected, msg.Line)
76 | }
77 |
78 | if len(msg.Extra) != 2 {
79 | t.Errorf("wrong number of extra fields (exp: %d, got %d) in %v", 2, len(msg.Extra), msg.Extra)
80 | }
81 |
82 | extra := map[string]interface{}{"foo": "bar", "withField": "1"}
83 |
84 | for k, v := range extra {
85 | // Remember extra fields are prefixed with "_"
86 | if msg.Extra["_"+k].(string) != extra[k].(string) {
87 | t.Errorf("Expected extra '%s' to be %#v, got %#v", k, v, msg.Extra["_"+k])
88 | }
89 | }
90 | }
91 |
92 | func TestWritingToTCP(t *testing.T) {
93 | listener, err := NewTCPReader("127.0.0.1:0")
94 | if err != nil {
95 | t.Fatalf("NewTCPReader: %s", err)
96 | }
97 | msgData := "test message\nsecond line"
98 | wg := &sync.WaitGroup{}
99 | wg.Add(1)
100 |
101 | go func(msgData string, wg *sync.WaitGroup) {
102 | r := new(Reader)
103 | r.conn, err = listener.Accept()
104 | if err != nil {
105 | fmt.Println(err)
106 | }
107 |
108 | msg, err := r.ReadMessage()
109 |
110 | if err != nil {
111 | t.Errorf("ReadMessage: %s", err)
112 | }
113 |
114 | if msg.Short != "test message" {
115 | t.Errorf("msg.Short: expected %s, got %s", msgData, msg.Full)
116 | }
117 |
118 | if msg.Full != msgData {
119 | t.Errorf("msg.Full: expected %s, got %s", msgData, msg.Full)
120 | }
121 |
122 | if msg.Level != SyslogInfoLevel {
123 | t.Errorf("msg.Level: expected: %d, got %d)", SyslogInfoLevel, msg.Level)
124 | }
125 |
126 | if msg.Host != "testing.local" {
127 | t.Errorf("Host should match (exp: testing.local, got: %s)", msg.Host)
128 | }
129 |
130 | if len(msg.Extra) != 2 {
131 | t.Errorf("wrong number of extra fields (exp: %d, got %d) in %v", 5, len(msg.Extra), msg.Extra)
132 | }
133 |
134 | fileExpected := ""
135 | if msg.File != fileExpected {
136 | t.Errorf("msg.File: expected %s, got %s", fileExpected,
137 | msg.File)
138 | }
139 |
140 | lineExpected := 0
141 | if msg.Line != lineExpected {
142 | t.Errorf("msg.Line: expected %d, got %d", lineExpected, msg.Line)
143 | }
144 |
145 | if len(msg.Extra) != 2 {
146 | t.Errorf("wrong number of extra fields (exp: %d, got %d) in %v", 2, len(msg.Extra), msg.Extra)
147 | }
148 |
149 | extra := map[string]interface{}{"foo": "bar", "withField": "1"}
150 |
151 | for k, v := range extra {
152 | // Remember extra fields are prefixed with "_"
153 | if msg.Extra["_"+k].(string) != extra[k].(string) {
154 | t.Errorf("Expected extra '%s' to be %#v, got %#v", k, v, msg.Extra["_"+k])
155 | }
156 | }
157 | wg.Done()
158 | }(msgData, wg)
159 |
160 | hook := NewGraylogHook("tcp://"+listener.Addr().String(), map[string]interface{}{"foo": "bar"})
161 | hook.Host = "testing.local"
162 | hook.Blacklist([]string{"filterMe"})
163 |
164 | log := logrus.New()
165 | log.Out = io.Discard
166 | log.Hooks.Add(hook)
167 | log.WithFields(logrus.Fields{"withField": "1", "filterMe": "1"}).Info(msgData)
168 | wg.Wait()
169 | fmt.Println("test done")
170 | }
171 |
172 | func TestErrorLevelReporting(t *testing.T) {
173 | r, err := NewUDPReader("127.0.0.1:0")
174 | if err != nil {
175 | t.Fatalf("NewUDPReader: %s", err)
176 | }
177 | hook := NewGraylogHook(r.Addr(), map[string]interface{}{"foo": "bar"})
178 | msgData := "test message\nsecond line"
179 |
180 | log := logrus.New()
181 | log.Out = io.Discard
182 | log.Hooks.Add(hook)
183 |
184 | log.Error(msgData)
185 |
186 | msg, err := r.ReadMessage()
187 |
188 | if err != nil {
189 | t.Errorf("ReadMessage: %s", err)
190 | }
191 |
192 | if msg.Short != "test message" {
193 | t.Errorf("msg.Short: expected %s, got %s", msgData, msg.Full)
194 | }
195 |
196 | if msg.Full != msgData {
197 | t.Errorf("msg.Full: expected %s, got %s", msgData, msg.Full)
198 | }
199 |
200 | if msg.Level != SyslogErrorLevel {
201 | t.Errorf("msg.Level: expected: %d, got %d)", SyslogErrorLevel, msg.Level)
202 | }
203 | }
204 |
205 | func TestJSONErrorMarshalling(t *testing.T) {
206 | r, err := NewUDPReader("127.0.0.1:0")
207 | if err != nil {
208 | t.Fatalf("NewUDPReader: %s", err)
209 | }
210 | hook := NewGraylogHook(r.Addr(), map[string]interface{}{})
211 |
212 | log := logrus.New()
213 | log.Out = io.Discard
214 | log.Hooks.Add(hook)
215 |
216 | log.WithError(errors.New("sample error")).Info("Testing sample error")
217 |
218 | msg, err := r.ReadMessage()
219 | if err != nil {
220 | t.Errorf("ReadMessage: %s", err)
221 | }
222 |
223 | encoded, err := json.Marshal(msg)
224 | if err != nil {
225 | t.Errorf("Marshaling json: %s", err)
226 | }
227 |
228 | errSection := regexp.MustCompile(`"_error":"sample error"`)
229 | if !errSection.MatchString(string(encoded)) {
230 | t.Errorf("Expected error message to be encoded into message")
231 | }
232 | }
233 |
234 | func TestParallelLogging(t *testing.T) {
235 | r, err := NewUDPReader("127.0.0.1:0")
236 | if err != nil {
237 | t.Fatalf("NewUDPReader: %s", err)
238 | }
239 | hook := NewGraylogHook(r.Addr(), nil)
240 | asyncHook := NewAsyncGraylogHook(r.Addr(), nil)
241 |
242 | log := logrus.New()
243 | log.Out = io.Discard
244 | log.Hooks.Add(hook)
245 | log.Hooks.Add(asyncHook)
246 |
247 | log2 := logrus.New()
248 | log2.Out = io.Discard
249 | log2.Hooks.Add(hook)
250 | log2.Hooks.Add(asyncHook)
251 |
252 | quit := make(chan struct{})
253 | defer close(quit)
254 |
255 | recordPanic := func() {
256 | if r := recover(); r != nil {
257 | t.Fatalf("Logging in parallel caused a panic")
258 | }
259 | }
260 |
261 | var wg sync.WaitGroup
262 |
263 | // Start draining messages from GELF
264 | go func() {
265 | defer recordPanic()
266 | for {
267 | select {
268 | case <-quit:
269 | return
270 | default:
271 | r.ReadMessage()
272 | }
273 | }
274 | }()
275 |
276 | // Log into our hook in parallel
277 | for i := 0; i < 100; i++ {
278 | wg.Add(1)
279 |
280 | go func() {
281 | defer wg.Done()
282 | defer recordPanic()
283 |
284 | log.Info("Logging")
285 | log2.Info("Logging from another logger")
286 | }()
287 | }
288 |
289 | wg.Wait()
290 | }
291 |
292 | func TestSetWriter(t *testing.T) {
293 | r, err := NewUDPReader("127.0.0.1:0")
294 | if err != nil {
295 | t.Fatalf("NewUDPReader: %s", err)
296 | }
297 | hook := NewGraylogHook(r.Addr(), nil)
298 |
299 | w := hook.Writer().(*LowLevelProtocolWriter)
300 | w.CompressionLevel = flate.BestCompression
301 | hook.SetWriter(w)
302 |
303 | if hook.Writer().(*LowLevelProtocolWriter).CompressionLevel != flate.BestCompression {
304 | t.Error("UDPWriter was not set correctly")
305 | }
306 |
307 | if hook.SetWriter(nil) == nil {
308 | t.Error("Setting a nil writer should raise an error")
309 | }
310 | }
311 |
312 | func TestWithInvalidGraylogAddr(t *testing.T) {
313 | addr, err := net.ResolveUDPAddr("udp", "localhost:0")
314 | if err != nil {
315 | panic(err)
316 | }
317 | logrus.SetOutput(io.Discard)
318 | hook := NewGraylogHook(addr.String(), nil)
319 |
320 | log := logrus.New()
321 | log.Out = io.Discard
322 | log.Hooks.Add(hook)
323 |
324 | // Should not panic
325 | log.WithError(errors.New("sample error")).Info("Testing sample error")
326 | }
327 |
328 | func TestStackTracer(t *testing.T) {
329 | r, err := NewUDPReader("127.0.0.1:0")
330 | if err != nil {
331 | t.Fatalf("NewUDPReader: %s", err)
332 | }
333 | hook := NewGraylogHook(r.Addr(), map[string]interface{}{})
334 |
335 | log := logrus.New()
336 | log.SetReportCaller(true)
337 | log.Out = io.Discard
338 | log.Hooks.Add(hook)
339 |
340 | stackErr := pkgerrors.New("sample error")
341 |
342 | log.WithError(stackErr).Info("Testing sample error")
343 |
344 | msg, err := r.ReadMessage()
345 | if err != nil {
346 | t.Errorf("ReadMessage: %s", err)
347 | }
348 |
349 | fileExpected := "graylog_hook_test.go"
350 | if !strings.HasSuffix(msg.File, fileExpected) {
351 | t.Errorf("msg.File: expected %s, got %s", fileExpected,
352 | msg.File)
353 | }
354 |
355 | lineExpected := 342 // Update this if code is updated above
356 | if msg.Line != lineExpected {
357 | t.Errorf("msg.Line: expected %d, got %d", lineExpected, msg.Line)
358 | }
359 |
360 | stacktraceI, ok := msg.Extra[StackTraceKey]
361 | if !ok {
362 | t.Error("Stack Trace not found in result")
363 | }
364 | stacktrace, ok := stacktraceI.(string)
365 | if !ok {
366 | t.Error("Stack Trace is not a string")
367 | }
368 |
369 | // Run the test for stack trace only in stable versions
370 | if !strings.Contains(runtime.Version(), "devel") {
371 | stacktraceRE := regexp.MustCompile(`^
372 | (.+)?logrus-graylog-hook(/v3)?.TestStackTracer
373 | (/|[A-Z]:/).+/logrus-graylog-hook(.v3)?/graylog_hook_test.go:\d+
374 | testing.tRunner
375 | (/|[A-Z]:/).*/testing.go:\d+
376 | runtime.*
377 | (/|[A-Z]:/).*/runtime/.*:\d+$`)
378 |
379 | if !stacktraceRE.MatchString(stacktrace) {
380 | t.Errorf("Stack Trace not as expected. Got:\n%s\n", stacktrace)
381 | }
382 | }
383 | }
384 |
385 | func TestLogrusLevelToSyslog(t *testing.T) {
386 | // Syslog constants
387 | const (
388 | LOG_EMERG = 0 /* system is unusable */
389 | LOG_ALERT = 1 /* action must be taken immediately */
390 | LOG_CRIT = 2 /* critical conditions */
391 | LOG_ERR = 3 /* error conditions */
392 | LOG_WARNING = 4 /* warning conditions */
393 | LOG_NOTICE = 5 /* normal but significant condition */
394 | LOG_INFO = 6 /* informational */
395 | LOG_DEBUG = 7 /* debug-level messages */
396 | )
397 |
398 | if logrusLevelToSyslog(logrus.TraceLevel) != LOG_DEBUG {
399 | t.Error("logrusLevelToSyslog(TraceLevel) != LOG_DEBUG")
400 | }
401 |
402 | if logrusLevelToSyslog(logrus.DebugLevel) != LOG_DEBUG {
403 | t.Error("logrusLevelToSyslog(DebugLevel) != LOG_DEBUG")
404 | }
405 |
406 | if logrusLevelToSyslog(logrus.InfoLevel) != LOG_INFO {
407 | t.Error("logrusLevelToSyslog(InfoLevel) != LOG_INFO")
408 | }
409 |
410 | if logrusLevelToSyslog(logrus.WarnLevel) != LOG_WARNING {
411 | t.Error("logrusLevelToSyslog(WarnLevel) != LOG_WARNING")
412 | }
413 |
414 | if logrusLevelToSyslog(logrus.ErrorLevel) != LOG_ERR {
415 | t.Error("logrusLevelToSyslog(ErrorLevel) != LOG_ERR")
416 | }
417 |
418 | if logrusLevelToSyslog(logrus.FatalLevel) != LOG_CRIT {
419 | t.Error("logrusLevelToSyslog(FatalLevel) != LOG_CRIT")
420 | }
421 |
422 | if logrusLevelToSyslog(logrus.PanicLevel) != LOG_ALERT {
423 | t.Error("logrusLevelToSyslog(PanicLevel) != LOG_ALERT")
424 | }
425 | }
426 |
427 | func TestReportCallerEnabled(t *testing.T) {
428 | r, err := NewUDPReader("127.0.0.1:0")
429 | if err != nil {
430 | t.Fatalf("NewUDPReader: %s", err)
431 | }
432 | hook := NewGraylogHook(r.Addr(), map[string]interface{}{})
433 | hook.Host = "testing.local"
434 | msgData := "test message\nsecond line"
435 |
436 | log := logrus.New()
437 | log.SetReportCaller(true)
438 | log.Out = io.Discard
439 | log.Hooks.Add(hook)
440 | log.Info(msgData)
441 |
442 | msg, err := r.ReadMessage()
443 |
444 | if err != nil {
445 | t.Errorf("ReadMessage: %s", err)
446 | }
447 |
448 | fileField, ok := msg.Extra["_file"]
449 | if !ok {
450 | t.Error("_file field not present in extra fields")
451 | }
452 |
453 | fileGot, ok := fileField.(string)
454 | if !ok {
455 | t.Error("_file field is not a string")
456 | }
457 |
458 | fileExpected := "graylog_hook_test.go"
459 | if !strings.HasSuffix(fileGot, fileExpected) {
460 | t.Errorf("msg.Extra[\"_file\"]: expected %s, got %s", fileExpected, fileGot)
461 | }
462 |
463 | lineField, ok := msg.Extra["_line"]
464 | if !ok {
465 | t.Error("_line field not present in extra fields")
466 | }
467 |
468 | lineGot, ok := lineField.(float64)
469 | if !ok {
470 | t.Error("_line dowes not have the correct type")
471 | }
472 |
473 | lineExpected := 440 // Update this if code is updated above
474 | if msg.Line != lineExpected {
475 | t.Errorf("msg.Extra[\"_line\"]: expected %d, got %d", lineExpected, int(lineGot))
476 | }
477 |
478 | functionField, ok := msg.Extra["_function"]
479 | if !ok {
480 | t.Error("_function field not present in extra fields")
481 | }
482 |
483 | functionGot, ok := functionField.(string)
484 | if !ok {
485 | t.Error("_function field is not a string")
486 | }
487 |
488 | functionExpected := "TestReportCallerEnabled"
489 | if !strings.HasSuffix(functionGot, functionExpected) {
490 | t.Errorf("msg.Extra[\"_function\"]: expected %s, got %s", functionExpected, functionGot)
491 | }
492 |
493 | gelfFileExpected := "graylog_hook_test.go"
494 | if !strings.HasSuffix(msg.File, gelfFileExpected) {
495 | t.Errorf("msg.File: expected %s, got %s", gelfFileExpected,
496 | msg.File)
497 | }
498 |
499 | gelfLineExpected := 440 // Update this if code is updated above
500 | if msg.Line != lineExpected {
501 | t.Errorf("msg.Line: expected %d, got %d", gelfLineExpected, msg.Line)
502 | }
503 | }
504 |
505 | func TestReportCallerDisabled(t *testing.T) {
506 | r, err := NewUDPReader("127.0.0.1:0")
507 | if err != nil {
508 | t.Fatalf("NewUDPReader: %s", err)
509 | }
510 | hook := NewGraylogHook(r.Addr(), map[string]interface{}{})
511 | hook.Host = "testing.local"
512 | msgData := "test message\nsecond line"
513 |
514 | log := logrus.New()
515 | log.SetReportCaller(false)
516 | log.Out = io.Discard
517 | log.Hooks.Add(hook)
518 | log.Info(msgData)
519 |
520 | msg, err := r.ReadMessage()
521 |
522 | if err != nil {
523 | t.Errorf("ReadMessage: %s", err)
524 | }
525 |
526 | if _, ok := msg.Extra["_file"]; ok {
527 | t.Error("_file field should not present in extra fields")
528 | }
529 |
530 | if _, ok := msg.Extra["_line"]; ok {
531 | t.Error("_line field should not present in extra fields")
532 | }
533 |
534 | if _, ok := msg.Extra["_function"]; ok {
535 | t.Error("_function field should not present in extra fields")
536 | }
537 |
538 | // if reportCaller is disabled (this is the default setting) the File and Line field should have the default values
539 | // corresponding to the types. "" and 0 respectively.
540 | gelfFileExpected := ""
541 | if msg.File != gelfFileExpected {
542 | t.Errorf("msg.File: expected %s, got %s", gelfFileExpected, msg.File)
543 | }
544 |
545 | gelfLineExpected := 0
546 | if msg.Line != gelfLineExpected {
547 | t.Errorf("msg.Line: expected %d, got %d", gelfLineExpected, msg.Line)
548 | }
549 | }
550 |
551 | func TestHTTPWriter(t *testing.T) {
552 | var gelf map[string]interface{}
553 |
554 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
555 | // Test request parameters
556 | all, err := ioutil.ReadAll(req.Body)
557 | if err != nil {
558 | t.Fatal("Unable to read response body")
559 | }
560 |
561 | err = json.Unmarshal(all, &gelf)
562 | if err != nil {
563 | t.Fatal("Unable to unmarshal json")
564 | }
565 |
566 | if gelf["host"] != "testing.local" {
567 | t.Errorf("host: expected %s, got %s", "testing.local", gelf["host"])
568 | }
569 |
570 | rw.WriteHeader(204)
571 | }))
572 | // Close the server when test finishes
573 | defer server.Close()
574 |
575 | hook := NewGraylogHook(server.URL, map[string]interface{}{})
576 | hook.Host = "testing.local"
577 | msgData := "test message\nsecond line"
578 |
579 | log := logrus.New()
580 | log.SetReportCaller(false)
581 | log.Out = io.Discard
582 | log.Hooks.Add(hook)
583 | log.Info(msgData)
584 | }
585 |
--------------------------------------------------------------------------------