Browse code

Merge pull request #34758 from ghislainbourgeois/33495-add-tcp-to-gelf-log-driver

Add TCP support for GELF log driver

Brian Goff authored on 2017/10/10 23:26:01
Showing 11 changed files
... ...
@@ -23,7 +23,7 @@ import (
23 23
 const name = "gelf"
24 24
 
25 25
 type gelfLogger struct {
26
-	writer   *gelf.Writer
26
+	writer   gelf.Writer
27 27
 	info     logger.Info
28 28
 	hostname string
29 29
 	rawExtra json.RawMessage
... ...
@@ -89,8 +89,56 @@ func New(info logger.Info) (logger.Logger, error) {
89 89
 		return nil, err
90 90
 	}
91 91
 
92
-	// create new gelfWriter
93
-	gelfWriter, err := gelf.NewWriter(address)
92
+	var gelfWriter gelf.Writer
93
+	if address.Scheme == "udp" {
94
+		gelfWriter, err = newGELFUDPWriter(address.Host, info)
95
+		if err != nil {
96
+			return nil, err
97
+		}
98
+	} else if address.Scheme == "tcp" {
99
+		gelfWriter, err = newGELFTCPWriter(address.Host, info)
100
+		if err != nil {
101
+			return nil, err
102
+		}
103
+	}
104
+
105
+	return &gelfLogger{
106
+		writer:   gelfWriter,
107
+		info:     info,
108
+		hostname: hostname,
109
+		rawExtra: rawExtra,
110
+	}, nil
111
+}
112
+
113
+// create new TCP gelfWriter
114
+func newGELFTCPWriter(address string, info logger.Info) (gelf.Writer, error) {
115
+	gelfWriter, err := gelf.NewTCPWriter(address)
116
+	if err != nil {
117
+		return nil, fmt.Errorf("gelf: cannot connect to GELF endpoint: %s %v", address, err)
118
+	}
119
+
120
+	if v, ok := info.Config["gelf-tcp-max-reconnect"]; ok {
121
+		i, err := strconv.Atoi(v)
122
+		if err != nil || i < 0 {
123
+			return nil, fmt.Errorf("gelf-tcp-max-reconnect must be a positive integer")
124
+		}
125
+		gelfWriter.MaxReconnect = i
126
+	}
127
+
128
+	if v, ok := info.Config["gelf-tcp-reconnect-delay"]; ok {
129
+		i, err := strconv.Atoi(v)
130
+		if err != nil || i < 0 {
131
+			return nil, fmt.Errorf("gelf-tcp-reconnect-delay must be a positive integer")
132
+		}
133
+		gelfWriter.ReconnectDelay = time.Duration(i)
134
+	}
135
+
136
+	return gelfWriter, nil
137
+}
138
+
139
+// create new UDP gelfWriter
140
+func newGELFUDPWriter(address string, info logger.Info) (gelf.Writer, error) {
141
+	gelfWriter, err := gelf.NewUDPWriter(address)
94 142
 	if err != nil {
95 143
 		return nil, fmt.Errorf("gelf: cannot connect to GELF endpoint: %s %v", address, err)
96 144
 	}
... ...
@@ -116,12 +164,7 @@ func New(info logger.Info) (logger.Logger, error) {
116 116
 		gelfWriter.CompressionLevel = val
117 117
 	}
118 118
 
119
-	return &gelfLogger{
120
-		writer:   gelfWriter,
121
-		info:     info,
122
-		hostname: hostname,
123
-		rawExtra: rawExtra,
124
-	}, nil
119
+	return gelfWriter, nil
125 120
 }
126 121
 
127 122
 func (s *gelfLogger) Log(msg *logger.Message) error {
... ...
@@ -135,7 +178,7 @@ func (s *gelfLogger) Log(msg *logger.Message) error {
135 135
 		Host:     s.hostname,
136 136
 		Short:    string(msg.Line),
137 137
 		TimeUnix: float64(msg.Timestamp.UnixNano()/int64(time.Millisecond)) / 1000.0,
138
-		Level:    level,
138
+		Level:    int32(level),
139 139
 		RawExtra: s.rawExtra,
140 140
 	}
141 141
 	logger.PutMessage(msg)
... ...
@@ -156,6 +199,11 @@ func (s *gelfLogger) Name() string {
156 156
 
157 157
 // ValidateLogOpt looks for gelf specific log option gelf-address.
158 158
 func ValidateLogOpt(cfg map[string]string) error {
159
+	address, err := parseAddress(cfg["gelf-address"])
160
+	if err != nil {
161
+		return err
162
+	}
163
+
159 164
 	for key, val := range cfg {
160 165
 		switch key {
161 166
 		case "gelf-address":
... ...
@@ -164,46 +212,59 @@ func ValidateLogOpt(cfg map[string]string) error {
164 164
 		case "env":
165 165
 		case "env-regex":
166 166
 		case "gelf-compression-level":
167
+			if address.Scheme != "udp" {
168
+				return fmt.Errorf("compression is only supported on UDP")
169
+			}
167 170
 			i, err := strconv.Atoi(val)
168 171
 			if err != nil || i < flate.DefaultCompression || i > flate.BestCompression {
169 172
 				return fmt.Errorf("unknown value %q for log opt %q for gelf log driver", val, key)
170 173
 			}
171 174
 		case "gelf-compression-type":
175
+			if address.Scheme != "udp" {
176
+				return fmt.Errorf("compression is only supported on UDP")
177
+			}
172 178
 			switch val {
173 179
 			case "gzip", "zlib", "none":
174 180
 			default:
175 181
 				return fmt.Errorf("unknown value %q for log opt %q for gelf log driver", val, key)
176 182
 			}
183
+		case "gelf-tcp-max-reconnect", "gelf-tcp-reconnect-delay":
184
+			if address.Scheme != "tcp" {
185
+				return fmt.Errorf("%q is only valid for TCP", key)
186
+			}
187
+			i, err := strconv.Atoi(val)
188
+			if err != nil || i < 0 {
189
+				return fmt.Errorf("%q must be a positive integer", key)
190
+			}
177 191
 		default:
178 192
 			return fmt.Errorf("unknown log opt %q for gelf log driver", key)
179 193
 		}
180 194
 	}
181 195
 
182
-	_, err := parseAddress(cfg["gelf-address"])
183
-	return err
196
+	return nil
184 197
 }
185 198
 
186
-func parseAddress(address string) (string, error) {
199
+func parseAddress(address string) (*url.URL, error) {
187 200
 	if address == "" {
188
-		return "", nil
201
+		return nil, fmt.Errorf("gelf-address is a required parameter")
189 202
 	}
190 203
 	if !urlutil.IsTransportURL(address) {
191
-		return "", fmt.Errorf("gelf-address should be in form proto://address, got %v", address)
204
+		return nil, fmt.Errorf("gelf-address should be in form proto://address, got %v", address)
192 205
 	}
193 206
 	url, err := url.Parse(address)
194 207
 	if err != nil {
195
-		return "", err
208
+		return nil, err
196 209
 	}
197 210
 
198 211
 	// we support only udp
199
-	if url.Scheme != "udp" {
200
-		return "", fmt.Errorf("gelf: endpoint needs to be UDP")
212
+	if url.Scheme != "udp" && url.Scheme != "tcp" {
213
+		return nil, fmt.Errorf("gelf: endpoint needs to be TCP or UDP")
201 214
 	}
202 215
 
203 216
 	// get host and port
204 217
 	if _, _, err = net.SplitHostPort(url.Host); err != nil {
205
-		return "", fmt.Errorf("gelf: please provide gelf-address as udp://host:port")
218
+		return nil, fmt.Errorf("gelf: please provide gelf-address as proto://host:port")
206 219
 	}
207 220
 
208
-	return url.Host, nil
221
+	return url, nil
209 222
 }
210 223
new file mode 100644
... ...
@@ -0,0 +1,260 @@
0
+// +build linux
1
+
2
+package gelf
3
+
4
+import (
5
+	"net"
6
+	"testing"
7
+
8
+	"github.com/docker/docker/daemon/logger"
9
+)
10
+
11
+// Validate parseAddress
12
+func TestParseAddress(t *testing.T) {
13
+	url, err := parseAddress("udp://127.0.0.1:12201")
14
+	if err != nil {
15
+		t.Fatal(err)
16
+	}
17
+	if url.String() != "udp://127.0.0.1:12201" {
18
+		t.Fatalf("Expected address udp://127.0.0.1:12201, got %s", url.String())
19
+	}
20
+
21
+	_, err = parseAddress("127.0.0.1:12201")
22
+	if err == nil {
23
+		t.Fatal("Expected error requiring protocol")
24
+	}
25
+
26
+	_, err = parseAddress("http://127.0.0.1:12201")
27
+	if err == nil {
28
+		t.Fatal("Expected error restricting protocol")
29
+	}
30
+}
31
+
32
+// Validate TCP options
33
+func TestTCPValidateLogOpt(t *testing.T) {
34
+	err := ValidateLogOpt(map[string]string{
35
+		"gelf-address": "tcp://127.0.0.1:12201",
36
+	})
37
+	if err != nil {
38
+		t.Fatal("Expected TCP to be supported")
39
+	}
40
+
41
+	err = ValidateLogOpt(map[string]string{
42
+		"gelf-address":           "tcp://127.0.0.1:12201",
43
+		"gelf-compression-level": "9",
44
+	})
45
+	if err == nil {
46
+		t.Fatal("Expected TCP to reject compression level")
47
+	}
48
+
49
+	err = ValidateLogOpt(map[string]string{
50
+		"gelf-address":          "tcp://127.0.0.1:12201",
51
+		"gelf-compression-type": "gzip",
52
+	})
53
+	if err == nil {
54
+		t.Fatal("Expected TCP to reject compression type")
55
+	}
56
+
57
+	err = ValidateLogOpt(map[string]string{
58
+		"gelf-address":             "tcp://127.0.0.1:12201",
59
+		"gelf-tcp-max-reconnect":   "5",
60
+		"gelf-tcp-reconnect-delay": "10",
61
+	})
62
+	if err != nil {
63
+		t.Fatal("Expected TCP reconnect to be a valid parameters")
64
+	}
65
+
66
+	err = ValidateLogOpt(map[string]string{
67
+		"gelf-address":             "tcp://127.0.0.1:12201",
68
+		"gelf-tcp-max-reconnect":   "-1",
69
+		"gelf-tcp-reconnect-delay": "-3",
70
+	})
71
+	if err == nil {
72
+		t.Fatal("Expected negative TCP reconnect to be rejected")
73
+	}
74
+
75
+	err = ValidateLogOpt(map[string]string{
76
+		"gelf-address":             "tcp://127.0.0.1:12201",
77
+		"gelf-tcp-max-reconnect":   "invalid",
78
+		"gelf-tcp-reconnect-delay": "invalid",
79
+	})
80
+	if err == nil {
81
+		t.Fatal("Expected TCP reconnect to be required to be an int")
82
+	}
83
+
84
+	err = ValidateLogOpt(map[string]string{
85
+		"gelf-address":             "udp://127.0.0.1:12201",
86
+		"gelf-tcp-max-reconnect":   "1",
87
+		"gelf-tcp-reconnect-delay": "3",
88
+	})
89
+	if err == nil {
90
+		t.Fatal("Expected TCP reconnect to be invalid for UDP")
91
+	}
92
+}
93
+
94
+// Validate UDP options
95
+func TestUDPValidateLogOpt(t *testing.T) {
96
+	err := ValidateLogOpt(map[string]string{
97
+		"gelf-address":           "udp://127.0.0.1:12201",
98
+		"tag":                    "testtag",
99
+		"labels":                 "testlabel",
100
+		"env":                    "testenv",
101
+		"env-regex":              "testenv-regex",
102
+		"gelf-compression-level": "9",
103
+		"gelf-compression-type":  "gzip",
104
+	})
105
+	if err != nil {
106
+		t.Fatal(err)
107
+	}
108
+
109
+	err = ValidateLogOpt(map[string]string{
110
+		"gelf-address":           "udp://127.0.0.1:12201",
111
+		"gelf-compression-level": "ultra",
112
+		"gelf-compression-type":  "zlib",
113
+	})
114
+	if err == nil {
115
+		t.Fatal("Expected compression level error")
116
+	}
117
+
118
+	err = ValidateLogOpt(map[string]string{
119
+		"gelf-address":          "udp://127.0.0.1:12201",
120
+		"gelf-compression-type": "rar",
121
+	})
122
+	if err == nil {
123
+		t.Fatal("Expected compression type error")
124
+	}
125
+
126
+	err = ValidateLogOpt(map[string]string{
127
+		"invalid": "invalid",
128
+	})
129
+	if err == nil {
130
+		t.Fatal("Expected unknown option error")
131
+	}
132
+
133
+	err = ValidateLogOpt(map[string]string{})
134
+	if err == nil {
135
+		t.Fatal("Expected required parameter error")
136
+	}
137
+}
138
+
139
+// Validate newGELFTCPWriter
140
+func TestNewGELFTCPWriter(t *testing.T) {
141
+	address := "127.0.0.1:0"
142
+	tcpAddr, err := net.ResolveTCPAddr("tcp", address)
143
+	if err != nil {
144
+		t.Fatal(err)
145
+	}
146
+
147
+	listener, err := net.ListenTCP("tcp", tcpAddr)
148
+	if err != nil {
149
+		t.Fatal(err)
150
+	}
151
+
152
+	url := "tcp://" + listener.Addr().String()
153
+	info := logger.Info{
154
+		Config: map[string]string{
155
+			"gelf-address":             url,
156
+			"gelf-tcp-max-reconnect":   "0",
157
+			"gelf-tcp-reconnect-delay": "0",
158
+			"tag": "{{.ID}}",
159
+		},
160
+		ContainerID: "12345678901234567890",
161
+	}
162
+
163
+	writer, err := newGELFTCPWriter(listener.Addr().String(), info)
164
+	if err != nil {
165
+		t.Fatal(err)
166
+	}
167
+
168
+	err = writer.Close()
169
+	if err != nil {
170
+		t.Fatal(err)
171
+	}
172
+
173
+	err = listener.Close()
174
+	if err != nil {
175
+		t.Fatal(err)
176
+	}
177
+}
178
+
179
+// Validate newGELFUDPWriter
180
+func TestNewGELFUDPWriter(t *testing.T) {
181
+	address := "127.0.0.1:0"
182
+	info := logger.Info{
183
+		Config: map[string]string{
184
+			"gelf-address":           "udp://127.0.0.1:0",
185
+			"gelf-compression-level": "5",
186
+			"gelf-compression-type":  "gzip",
187
+		},
188
+	}
189
+
190
+	writer, err := newGELFUDPWriter(address, info)
191
+	if err != nil {
192
+		t.Fatal(err)
193
+	}
194
+	writer.Close()
195
+	if err != nil {
196
+		t.Fatal(err)
197
+	}
198
+}
199
+
200
+// Validate New for TCP
201
+func TestNewTCP(t *testing.T) {
202
+	address := "127.0.0.1:0"
203
+	tcpAddr, err := net.ResolveTCPAddr("tcp", address)
204
+	if err != nil {
205
+		t.Fatal(err)
206
+	}
207
+
208
+	listener, err := net.ListenTCP("tcp", tcpAddr)
209
+	if err != nil {
210
+		t.Fatal(err)
211
+	}
212
+
213
+	url := "tcp://" + listener.Addr().String()
214
+	info := logger.Info{
215
+		Config: map[string]string{
216
+			"gelf-address":             url,
217
+			"gelf-tcp-max-reconnect":   "0",
218
+			"gelf-tcp-reconnect-delay": "0",
219
+		},
220
+		ContainerID: "12345678901234567890",
221
+	}
222
+
223
+	logger, err := New(info)
224
+	if err != nil {
225
+		t.Fatal(err)
226
+	}
227
+
228
+	err = logger.Close()
229
+	if err != nil {
230
+		t.Fatal(err)
231
+	}
232
+
233
+	err = listener.Close()
234
+	if err != nil {
235
+		t.Fatal(err)
236
+	}
237
+}
238
+
239
+// Validate New for UDP
240
+func TestNewUDP(t *testing.T) {
241
+	info := logger.Info{
242
+		Config: map[string]string{
243
+			"gelf-address":           "udp://127.0.0.1:0",
244
+			"gelf-compression-level": "5",
245
+			"gelf-compression-type":  "gzip",
246
+		},
247
+		ContainerID: "12345678901234567890",
248
+	}
249
+
250
+	logger, err := New(info)
251
+	if err != nil {
252
+		t.Fatal(err)
253
+	}
254
+
255
+	err = logger.Close()
256
+	if err != nil {
257
+		t.Fatal(err)
258
+	}
259
+}
0 260
deleted file mode 100644
... ...
@@ -1,3 +0,0 @@
1
-// +build !linux
2
-
3
-package gelf
... ...
@@ -79,7 +79,7 @@ github.com/syndtr/gocapability 2c00daeb6c3b45114c80ac44119e7b8801fdd852
79 79
 github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4
80 80
 
81 81
 # gelf logging driver deps
82
-github.com/Graylog2/go-gelf 7029da823dad4ef3a876df61065156acb703b2ea
82
+github.com/Graylog2/go-gelf v2
83 83
 
84 84
 github.com/fluent/fluent-logger-golang v1.2.1
85 85
 # fluent-logger-golang deps
... ...
@@ -1,17 +1,53 @@
1
-go-gelf - GELF library and writer for Go
1
+go-gelf - GELF Library and Writer for Go
2 2
 ========================================
3 3
 
4
-GELF is graylog2's UDP logging format.  This library provides an API
5
-that applications can use to log messages directly to a graylog2
6
-server, along with an `io.Writer` that can be use to redirect the
7
-standard library's log messages (or `os.Stdout`), to a graylog2 server.
4
+[GELF] (Graylog Extended Log Format) is an application-level logging
5
+protocol that avoids many of the shortcomings of [syslog]. While it
6
+can be run over any stream or datagram transport protocol, it has
7
+special support ([chunking]) to allow long messages to be split over
8
+multiple datagrams.
9
+
10
+Versions
11
+--------
12
+
13
+In order to enable versionning of this package with Go, this project
14
+is using GoPkg.in. The default branch of this project will be v1
15
+for some time to prevent breaking clients. We encourage all project
16
+to change their imports to the new GoPkg.in URIs as soon as possible.
17
+
18
+To see up to date code, make sure to switch to the master branch.
19
+
20
+v1.0.0
21
+------
22
+
23
+This implementation currently supports UDP and TCP as a transport
24
+protocol. TLS is unsupported.
25
+
26
+The library provides an API that applications can use to log messages
27
+directly to a Graylog server and an `io.Writer` that can be used to
28
+redirect the standard library's log messages (`os.Stdout`) to a
29
+Graylog server.
30
+
31
+[GELF]: http://docs.graylog.org/en/2.2/pages/gelf.html
32
+[syslog]: https://tools.ietf.org/html/rfc5424
33
+[chunking]: http://docs.graylog.org/en/2.2/pages/gelf.html#chunked-gelf
34
+
8 35
 
9 36
 Installing
10 37
 ----------
11 38
 
12 39
 go-gelf is go get-able:
13 40
 
14
-	go get github.com/Graylog2/go-gelf/gelf
41
+    go get gopkg.in/Graylog2/go-gelf.v1/gelf
42
+
43
+    or
44
+
45
+    go get github.com/Graylog2/go-gelf/gelf
46
+
47
+This will get you version 1.0.0, with only UDP support and legacy API.
48
+Newer versions are available through GoPkg.in:
49
+
50
+    go get gopkg.in/Graylog2/go-gelf.v2/gelf
15 51
 
16 52
 Usage
17 53
 -----
... ...
@@ -21,50 +57,55 @@ having your `main` function (or even `init`) call `log.SetOutput()`.
21 21
 By using an `io.MultiWriter`, we can log to both stdout and graylog -
22 22
 giving us both centralized and local logs.  (Redundancy is nice).
23 23
 
24
-	package main
25
-
26
-	import (
27
-		"flag"
28
-		"github.com/Graylog2/go-gelf/gelf"
29
-		"io"
30
-		"log"
31
-		"os"
32
-	)
33
-
34
-	func main() {
35
-		var graylogAddr string
36
-
37
-		flag.StringVar(&graylogAddr, "graylog", "", "graylog server addr")
38
-		flag.Parse()
39
-
40
-		if graylogAddr != "" {
41
-			gelfWriter, err := gelf.NewWriter(graylogAddr)
42
-			if err != nil {
43
-				log.Fatalf("gelf.NewWriter: %s", err)
44
-			}
45
-			// log to both stderr and graylog2
46
-			log.SetOutput(io.MultiWriter(os.Stderr, gelfWriter))
47
-			log.Printf("logging to stderr & graylog2@'%s'", graylogAddr)
48
-		}
49
-
50
-		// From here on out, any calls to log.Print* functions
51
-		// will appear on stdout, and be sent over UDP to the
52
-		// specified Graylog2 server.
53
-
54
-		log.Printf("Hello gray World")
55
-
56
-		// ...
57
-	}
58
-
24
+```golang
25
+package main
26
+
27
+import (
28
+  "flag"
29
+  "gopkg.in/Graylog2/go-gelf.v2/gelf"
30
+  "io"
31
+  "log"
32
+  "os"
33
+)
34
+
35
+func main() {
36
+  var graylogAddr string
37
+
38
+  flag.StringVar(&graylogAddr, "graylog", "", "graylog server addr")
39
+  flag.Parse()
40
+
41
+  if graylogAddr != "" {
42
+          // If using UDP
43
+    gelfWriter, err := gelf.NewUDPWriter(graylogAddr)
44
+          // If using TCP
45
+          //gelfWriter, err := gelf.NewTCPWriter(graylogAddr)
46
+    if err != nil {
47
+      log.Fatalf("gelf.NewWriter: %s", err)
48
+    }
49
+    // log to both stderr and graylog2
50
+    log.SetOutput(io.MultiWriter(os.Stderr, gelfWriter))
51
+    log.Printf("logging to stderr & graylog2@'%s'", graylogAddr)
52
+  }
53
+
54
+  // From here on out, any calls to log.Print* functions
55
+  // will appear on stdout, and be sent over UDP or TCP to the
56
+  // specified Graylog2 server.
57
+
58
+  log.Printf("Hello gray World")
59
+
60
+  // ...
61
+}
62
+```
59 63
 The above program can be invoked as:
60 64
 
61
-	go run test.go -graylog=localhost:12201
65
+    go run test.go -graylog=localhost:12201
62 66
 
63
-Because GELF messages are sent over UDP, graylog server availability
64
-doesn't impact application performance or response time.  There is a
65
-small, fixed overhead per log call, regardless of whether the target
67
+When using UDP messages may be dropped or re-ordered. However, Graylog
68
+server availability will not impact application performance; there is
69
+a small, fixed overhead per log call regardless of whether the target
66 70
 server is reachable or not.
67 71
 
72
+
68 73
 To Do
69 74
 -----
70 75
 
71 76
new file mode 100644
... ...
@@ -0,0 +1,147 @@
0
+package gelf
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"time"
6
+)
7
+
8
+// Message represents the contents of the GELF message.  It is gzipped
9
+// before sending.
10
+type Message struct {
11
+	Version  string                 `json:"version"`
12
+	Host     string                 `json:"host"`
13
+	Short    string                 `json:"short_message"`
14
+	Full     string                 `json:"full_message,omitempty"`
15
+	TimeUnix float64                `json:"timestamp"`
16
+	Level    int32                  `json:"level,omitempty"`
17
+	Facility string                 `json:"facility,omitempty"`
18
+	Extra    map[string]interface{} `json:"-"`
19
+	RawExtra json.RawMessage        `json:"-"`
20
+}
21
+
22
+// Syslog severity levels
23
+const (
24
+	LOG_EMERG   = 0
25
+	LOG_ALERT   = 1
26
+	LOG_CRIT    = 2
27
+	LOG_ERR     = 3
28
+	LOG_WARNING = 4
29
+	LOG_NOTICE  = 5
30
+	LOG_INFO    = 6
31
+	LOG_DEBUG   = 7
32
+)
33
+
34
+func (m *Message) MarshalJSONBuf(buf *bytes.Buffer) error {
35
+	b, err := json.Marshal(m)
36
+	if err != nil {
37
+		return err
38
+	}
39
+	// write up until the final }
40
+	if _, err = buf.Write(b[:len(b)-1]); err != nil {
41
+		return err
42
+	}
43
+	if len(m.Extra) > 0 {
44
+		eb, err := json.Marshal(m.Extra)
45
+		if err != nil {
46
+			return err
47
+		}
48
+		// merge serialized message + serialized extra map
49
+		if err = buf.WriteByte(','); err != nil {
50
+			return err
51
+		}
52
+		// write serialized extra bytes, without enclosing quotes
53
+		if _, err = buf.Write(eb[1 : len(eb)-1]); err != nil {
54
+			return err
55
+		}
56
+	}
57
+
58
+	if len(m.RawExtra) > 0 {
59
+		if err := buf.WriteByte(','); err != nil {
60
+			return err
61
+		}
62
+
63
+		// write serialized extra bytes, without enclosing quotes
64
+		if _, err = buf.Write(m.RawExtra[1 : len(m.RawExtra)-1]); err != nil {
65
+			return err
66
+		}
67
+	}
68
+
69
+	// write final closing quotes
70
+	return buf.WriteByte('}')
71
+}
72
+
73
+func (m *Message) UnmarshalJSON(data []byte) error {
74
+	i := make(map[string]interface{}, 16)
75
+	if err := json.Unmarshal(data, &i); err != nil {
76
+		return err
77
+	}
78
+	for k, v := range i {
79
+		if k[0] == '_' {
80
+			if m.Extra == nil {
81
+				m.Extra = make(map[string]interface{}, 1)
82
+			}
83
+			m.Extra[k] = v
84
+			continue
85
+		}
86
+		switch k {
87
+		case "version":
88
+			m.Version = v.(string)
89
+		case "host":
90
+			m.Host = v.(string)
91
+		case "short_message":
92
+			m.Short = v.(string)
93
+		case "full_message":
94
+			m.Full = v.(string)
95
+		case "timestamp":
96
+			m.TimeUnix = v.(float64)
97
+		case "level":
98
+			m.Level = int32(v.(float64))
99
+		case "facility":
100
+			m.Facility = v.(string)
101
+		}
102
+	}
103
+	return nil
104
+}
105
+
106
+func (m *Message) toBytes() (messageBytes []byte, err error) {
107
+	buf := newBuffer()
108
+	defer bufPool.Put(buf)
109
+	if err = m.MarshalJSONBuf(buf); err != nil {
110
+		return nil, err
111
+	}
112
+	messageBytes = buf.Bytes()
113
+	return messageBytes, nil
114
+}
115
+
116
+func constructMessage(p []byte, hostname string, facility string, file string, line int) (m *Message) {
117
+	// remove trailing and leading whitespace
118
+	p = bytes.TrimSpace(p)
119
+
120
+	// If there are newlines in the message, use the first line
121
+	// for the short message and set the full message to the
122
+	// original input.  If the input has no newlines, stick the
123
+	// whole thing in Short.
124
+	short := p
125
+	full := []byte("")
126
+	if i := bytes.IndexRune(p, '\n'); i > 0 {
127
+		short = p[:i]
128
+		full = p
129
+	}
130
+
131
+	m = &Message{
132
+		Version:  "1.1",
133
+		Host:     hostname,
134
+		Short:    string(short),
135
+		Full:     string(full),
136
+		TimeUnix: float64(time.Now().Unix()),
137
+		Level:    6, // info
138
+		Facility: facility,
139
+		Extra: map[string]interface{}{
140
+			"_file": file,
141
+			"_line": line,
142
+		},
143
+	}
144
+
145
+	return m
146
+}
0 147
new file mode 100644
... ...
@@ -0,0 +1,93 @@
0
+package gelf
1
+
2
+import (
3
+	"bufio"
4
+	"encoding/json"
5
+	"fmt"
6
+	"net"
7
+)
8
+
9
+type TCPReader struct {
10
+	listener *net.TCPListener
11
+	conn     net.Conn
12
+	messages chan []byte
13
+}
14
+
15
+func newTCPReader(addr string) (*TCPReader, chan string, error) {
16
+	var err error
17
+	tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
18
+	if err != nil {
19
+		return nil, nil, fmt.Errorf("ResolveTCPAddr('%s'): %s", addr, err)
20
+	}
21
+
22
+	listener, err := net.ListenTCP("tcp", tcpAddr)
23
+	if err != nil {
24
+		return nil, nil, fmt.Errorf("ListenTCP: %s", err)
25
+	}
26
+
27
+	r := &TCPReader{
28
+		listener: listener,
29
+		messages: make(chan []byte, 100), // Make a buffered channel with at most 100 messages
30
+	}
31
+
32
+	signal := make(chan string, 1)
33
+
34
+	go r.listenUntilCloseSignal(signal)
35
+
36
+	return r, signal, nil
37
+}
38
+
39
+func (r *TCPReader) listenUntilCloseSignal(signal chan string) {
40
+	defer func() { signal <- "done" }()
41
+	defer r.listener.Close()
42
+	for {
43
+		conn, err := r.listener.Accept()
44
+		if err != nil {
45
+			break
46
+		}
47
+		go handleConnection(conn, r.messages)
48
+		select {
49
+		case sig := <-signal:
50
+			if sig == "stop" {
51
+				break
52
+			}
53
+		default:
54
+		}
55
+	}
56
+}
57
+
58
+func (r *TCPReader) addr() string {
59
+	return r.listener.Addr().String()
60
+}
61
+
62
+func handleConnection(conn net.Conn, messages chan<- []byte) {
63
+	defer conn.Close()
64
+	reader := bufio.NewReader(conn)
65
+
66
+	var b []byte
67
+	var err error
68
+
69
+	for {
70
+		if b, err = reader.ReadBytes(0); err != nil {
71
+			continue
72
+		}
73
+		if len(b) > 0 {
74
+			messages <- b
75
+		}
76
+	}
77
+}
78
+
79
+func (r *TCPReader) readMessage() (*Message, error) {
80
+	b := <-r.messages
81
+
82
+	var msg Message
83
+	if err := json.Unmarshal(b[:len(b)-1], &msg); err != nil {
84
+		return nil, fmt.Errorf("json.Unmarshal: %s", err)
85
+	}
86
+
87
+	return &msg, nil
88
+}
89
+
90
+func (r *TCPReader) Close() {
91
+	r.listener.Close()
92
+}
0 93
new file mode 100644
... ...
@@ -0,0 +1,97 @@
0
+package gelf
1
+
2
+import (
3
+	"fmt"
4
+	"net"
5
+	"os"
6
+	"sync"
7
+	"time"
8
+)
9
+
10
+const (
11
+	DefaultMaxReconnect   = 3
12
+	DefaultReconnectDelay = 1
13
+)
14
+
15
+type TCPWriter struct {
16
+	GelfWriter
17
+	mu             sync.Mutex
18
+	MaxReconnect   int
19
+	ReconnectDelay time.Duration
20
+}
21
+
22
+func NewTCPWriter(addr string) (*TCPWriter, error) {
23
+	var err error
24
+	w := new(TCPWriter)
25
+	w.MaxReconnect = DefaultMaxReconnect
26
+	w.ReconnectDelay = DefaultReconnectDelay
27
+	w.proto = "tcp"
28
+	w.addr = addr
29
+
30
+	if w.conn, err = net.Dial("tcp", addr); err != nil {
31
+		return nil, err
32
+	}
33
+	if w.hostname, err = os.Hostname(); err != nil {
34
+		return nil, err
35
+	}
36
+
37
+	return w, nil
38
+}
39
+
40
+// WriteMessage sends the specified message to the GELF server
41
+// specified in the call to New().  It assumes all the fields are
42
+// filled out appropriately.  In general, clients will want to use
43
+// Write, rather than WriteMessage.
44
+func (w *TCPWriter) WriteMessage(m *Message) (err error) {
45
+	messageBytes, err := m.toBytes()
46
+	if err != nil {
47
+		return err
48
+	}
49
+
50
+	messageBytes = append(messageBytes, 0)
51
+
52
+	n, err := w.writeToSocketWithReconnectAttempts(messageBytes)
53
+	if err != nil {
54
+		return err
55
+	}
56
+	if n != len(messageBytes) {
57
+		return fmt.Errorf("bad write (%d/%d)", n, len(messageBytes))
58
+	}
59
+
60
+	return nil
61
+}
62
+
63
+func (w *TCPWriter) Write(p []byte) (n int, err error) {
64
+	file, line := getCallerIgnoringLogMulti(1)
65
+
66
+	m := constructMessage(p, w.hostname, w.Facility, file, line)
67
+
68
+	if err = w.WriteMessage(m); err != nil {
69
+		return 0, err
70
+	}
71
+
72
+	return len(p), nil
73
+}
74
+
75
+func (w *TCPWriter) writeToSocketWithReconnectAttempts(zBytes []byte) (n int, err error) {
76
+	var errConn error
77
+
78
+	w.mu.Lock()
79
+	for i := 0; n <= w.MaxReconnect; i++ {
80
+		errConn = nil
81
+
82
+		n, err = w.conn.Write(zBytes)
83
+		if err != nil {
84
+			time.Sleep(w.ReconnectDelay * time.Second)
85
+			w.conn, errConn = net.Dial("tcp", w.addr)
86
+		} else {
87
+			break
88
+		}
89
+	}
90
+	w.mu.Unlock()
91
+
92
+	if errConn != nil {
93
+		return 0, fmt.Errorf("Write Failed: %s\nReconnection failed: %s", err, errConn)
94
+	}
95
+	return n, nil
96
+}
0 97
new file mode 100644
... ...
@@ -0,0 +1,231 @@
0
+// Copyright 2012 SocialCode. All rights reserved.
1
+// Use of this source code is governed by the MIT
2
+// license that can be found in the LICENSE file.
3
+
4
+package gelf
5
+
6
+import (
7
+	"bytes"
8
+	"compress/flate"
9
+	"compress/gzip"
10
+	"compress/zlib"
11
+	"crypto/rand"
12
+	"fmt"
13
+	"io"
14
+	"net"
15
+	"os"
16
+	"path"
17
+	"sync"
18
+)
19
+
20
+type UDPWriter struct {
21
+	GelfWriter
22
+	CompressionLevel int // one of the consts from compress/flate
23
+	CompressionType  CompressType
24
+}
25
+
26
+// What compression type the writer should use when sending messages
27
+// to the graylog2 server
28
+type CompressType int
29
+
30
+const (
31
+	CompressGzip CompressType = iota
32
+	CompressZlib
33
+	CompressNone
34
+)
35
+
36
+// Used to control GELF chunking.  Should be less than (MTU - len(UDP
37
+// header)).
38
+//
39
+// TODO: generate dynamically using Path MTU Discovery?
40
+const (
41
+	ChunkSize        = 1420
42
+	chunkedHeaderLen = 12
43
+	chunkedDataLen   = ChunkSize - chunkedHeaderLen
44
+)
45
+
46
+var (
47
+	magicChunked = []byte{0x1e, 0x0f}
48
+	magicZlib    = []byte{0x78}
49
+	magicGzip    = []byte{0x1f, 0x8b}
50
+)
51
+
52
+// numChunks returns the number of GELF chunks necessary to transmit
53
+// the given compressed buffer.
54
+func numChunks(b []byte) int {
55
+	lenB := len(b)
56
+	if lenB <= ChunkSize {
57
+		return 1
58
+	}
59
+	return len(b)/chunkedDataLen + 1
60
+}
61
+
62
+// New returns a new GELF Writer.  This writer can be used to send the
63
+// output of the standard Go log functions to a central GELF server by
64
+// passing it to log.SetOutput()
65
+func NewUDPWriter(addr string) (*UDPWriter, error) {
66
+	var err error
67
+	w := new(UDPWriter)
68
+	w.CompressionLevel = flate.BestSpeed
69
+
70
+	if w.conn, err = net.Dial("udp", addr); err != nil {
71
+		return nil, err
72
+	}
73
+	if w.hostname, err = os.Hostname(); err != nil {
74
+		return nil, err
75
+	}
76
+
77
+	w.Facility = path.Base(os.Args[0])
78
+
79
+	return w, nil
80
+}
81
+
82
+// writes the gzip compressed byte array to the connection as a series
83
+// of GELF chunked messages.  The format is documented at
84
+// http://docs.graylog.org/en/2.1/pages/gelf.html as:
85
+//
86
+//     2-byte magic (0x1e 0x0f), 8 byte id, 1 byte sequence id, 1 byte
87
+//     total, chunk-data
88
+func (w *GelfWriter) writeChunked(zBytes []byte) (err error) {
89
+	b := make([]byte, 0, ChunkSize)
90
+	buf := bytes.NewBuffer(b)
91
+	nChunksI := numChunks(zBytes)
92
+	if nChunksI > 128 {
93
+		return fmt.Errorf("msg too large, would need %d chunks", nChunksI)
94
+	}
95
+	nChunks := uint8(nChunksI)
96
+	// use urandom to get a unique message id
97
+	msgId := make([]byte, 8)
98
+	n, err := io.ReadFull(rand.Reader, msgId)
99
+	if err != nil || n != 8 {
100
+		return fmt.Errorf("rand.Reader: %d/%s", n, err)
101
+	}
102
+
103
+	bytesLeft := len(zBytes)
104
+	for i := uint8(0); i < nChunks; i++ {
105
+		buf.Reset()
106
+		// manually write header.  Don't care about
107
+		// host/network byte order, because the spec only
108
+		// deals in individual bytes.
109
+		buf.Write(magicChunked) //magic
110
+		buf.Write(msgId)
111
+		buf.WriteByte(i)
112
+		buf.WriteByte(nChunks)
113
+		// slice out our chunk from zBytes
114
+		chunkLen := chunkedDataLen
115
+		if chunkLen > bytesLeft {
116
+			chunkLen = bytesLeft
117
+		}
118
+		off := int(i) * chunkedDataLen
119
+		chunk := zBytes[off : off+chunkLen]
120
+		buf.Write(chunk)
121
+
122
+		// write this chunk, and make sure the write was good
123
+		n, err := w.conn.Write(buf.Bytes())
124
+		if err != nil {
125
+			return fmt.Errorf("Write (chunk %d/%d): %s", i,
126
+				nChunks, err)
127
+		}
128
+		if n != len(buf.Bytes()) {
129
+			return fmt.Errorf("Write len: (chunk %d/%d) (%d/%d)",
130
+				i, nChunks, n, len(buf.Bytes()))
131
+		}
132
+
133
+		bytesLeft -= chunkLen
134
+	}
135
+
136
+	if bytesLeft != 0 {
137
+		return fmt.Errorf("error: %d bytes left after sending", bytesLeft)
138
+	}
139
+	return nil
140
+}
141
+
142
+// 1k bytes buffer by default
143
+var bufPool = sync.Pool{
144
+	New: func() interface{} {
145
+		return bytes.NewBuffer(make([]byte, 0, 1024))
146
+	},
147
+}
148
+
149
+func newBuffer() *bytes.Buffer {
150
+	b := bufPool.Get().(*bytes.Buffer)
151
+	if b != nil {
152
+		b.Reset()
153
+		return b
154
+	}
155
+	return bytes.NewBuffer(nil)
156
+}
157
+
158
+// WriteMessage sends the specified message to the GELF server
159
+// specified in the call to New().  It assumes all the fields are
160
+// filled out appropriately.  In general, clients will want to use
161
+// Write, rather than WriteMessage.
162
+func (w *UDPWriter) WriteMessage(m *Message) (err error) {
163
+	mBuf := newBuffer()
164
+	defer bufPool.Put(mBuf)
165
+	if err = m.MarshalJSONBuf(mBuf); err != nil {
166
+		return err
167
+	}
168
+	mBytes := mBuf.Bytes()
169
+
170
+	var (
171
+		zBuf   *bytes.Buffer
172
+		zBytes []byte
173
+	)
174
+
175
+	var zw io.WriteCloser
176
+	switch w.CompressionType {
177
+	case CompressGzip:
178
+		zBuf = newBuffer()
179
+		defer bufPool.Put(zBuf)
180
+		zw, err = gzip.NewWriterLevel(zBuf, w.CompressionLevel)
181
+	case CompressZlib:
182
+		zBuf = newBuffer()
183
+		defer bufPool.Put(zBuf)
184
+		zw, err = zlib.NewWriterLevel(zBuf, w.CompressionLevel)
185
+	case CompressNone:
186
+		zBytes = mBytes
187
+	default:
188
+		panic(fmt.Sprintf("unknown compression type %d",
189
+			w.CompressionType))
190
+	}
191
+	if zw != nil {
192
+		if err != nil {
193
+			return
194
+		}
195
+		if _, err = zw.Write(mBytes); err != nil {
196
+			zw.Close()
197
+			return
198
+		}
199
+		zw.Close()
200
+		zBytes = zBuf.Bytes()
201
+	}
202
+
203
+	if numChunks(zBytes) > 1 {
204
+		return w.writeChunked(zBytes)
205
+	}
206
+	n, err := w.conn.Write(zBytes)
207
+	if err != nil {
208
+		return
209
+	}
210
+	if n != len(zBytes) {
211
+		return fmt.Errorf("bad write (%d/%d)", n, len(zBytes))
212
+	}
213
+
214
+	return nil
215
+}
216
+
217
+// Write encodes the given string in a GELF message and sends it to
218
+// the server specified in New().
219
+func (w *UDPWriter) Write(p []byte) (n int, err error) {
220
+	// 1 for the function that called us.
221
+	file, line := getCallerIgnoringLogMulti(1)
222
+
223
+	m := constructMessage(p, w.hostname, w.Facility, file, line)
224
+
225
+	if err = w.WriteMessage(m); err != nil {
226
+		return 0, err
227
+	}
228
+
229
+	return len(p), nil
230
+}
0 231
new file mode 100644
... ...
@@ -0,0 +1,41 @@
0
+package gelf
1
+
2
+import (
3
+	"runtime"
4
+	"strings"
5
+)
6
+
7
+// getCaller returns the filename and the line info of a function
8
+// further down in the call stack.  Passing 0 in as callDepth would
9
+// return info on the function calling getCallerIgnoringLog, 1 the
10
+// parent function, and so on.  Any suffixes passed to getCaller are
11
+// path fragments like "/pkg/log/log.go", and functions in the call
12
+// stack from that file are ignored.
13
+func getCaller(callDepth int, suffixesToIgnore ...string) (file string, line int) {
14
+	// bump by 1 to ignore the getCaller (this) stackframe
15
+	callDepth++
16
+outer:
17
+	for {
18
+		var ok bool
19
+		_, file, line, ok = runtime.Caller(callDepth)
20
+		if !ok {
21
+			file = "???"
22
+			line = 0
23
+			break
24
+		}
25
+
26
+		for _, s := range suffixesToIgnore {
27
+			if strings.HasSuffix(file, s) {
28
+				callDepth++
29
+				continue outer
30
+			}
31
+		}
32
+		break
33
+	}
34
+	return
35
+}
36
+
37
+func getCallerIgnoringLogMulti(callDepth int) (string, int) {
38
+	// the +1 is to ignore this (getCallerIgnoringLogMulti) frame
39
+	return getCaller(callDepth+1, "/pkg/log/log.go", "/pkg/io/multi.go")
40
+}
... ...
@@ -5,414 +5,27 @@
5 5
 package gelf
6 6
 
7 7
 import (
8
-	"bytes"
9
-	"compress/flate"
10
-	"compress/gzip"
11
-	"compress/zlib"
12
-	"crypto/rand"
13
-	"encoding/json"
14
-	"fmt"
15
-	"io"
16 8
 	"net"
17
-	"os"
18
-	"path"
19
-	"runtime"
20
-	"strings"
21
-	"sync"
22
-	"time"
23 9
 )
24 10
 
11
+type Writer interface {
12
+	Close() error
13
+	Write([]byte) (int, error)
14
+	WriteMessage(*Message) error
15
+}
16
+
25 17
 // Writer implements io.Writer and is used to send both discrete
26 18
 // messages to a graylog2 server, or data from a stream-oriented
27 19
 // interface (like the functions in log).
28
-type Writer struct {
29
-	mu               sync.Mutex
30
-	conn             net.Conn
31
-	hostname         string
32
-	Facility         string // defaults to current process name
33
-	CompressionLevel int    // one of the consts from compress/flate
34
-	CompressionType  CompressType
35
-}
36
-
37
-// What compression type the writer should use when sending messages
38
-// to the graylog2 server
39
-type CompressType int
40
-
41
-const (
42
-	CompressGzip CompressType = iota
43
-	CompressZlib
44
-	CompressNone
45
-)
46
-
47
-// Message represents the contents of the GELF message.  It is gzipped
48
-// before sending.
49
-type Message struct {
50
-	Version  string                 `json:"version"`
51
-	Host     string                 `json:"host"`
52
-	Short    string                 `json:"short_message"`
53
-	Full     string                 `json:"full_message,omitempty"`
54
-	TimeUnix float64                `json:"timestamp"`
55
-	Level    int32                  `json:"level,omitempty"`
56
-	Facility string                 `json:"facility,omitempty"`
57
-	Extra    map[string]interface{} `json:"-"`
58
-	RawExtra json.RawMessage        `json:"-"`
59
-}
60
-
61
-// Used to control GELF chunking.  Should be less than (MTU - len(UDP
62
-// header)).
63
-//
64
-// TODO: generate dynamically using Path MTU Discovery?
65
-const (
66
-	ChunkSize        = 1420
67
-	chunkedHeaderLen = 12
68
-	chunkedDataLen   = ChunkSize - chunkedHeaderLen
69
-)
70
-
71
-var (
72
-	magicChunked = []byte{0x1e, 0x0f}
73
-	magicZlib    = []byte{0x78}
74
-	magicGzip    = []byte{0x1f, 0x8b}
75
-)
76
-
77
-// Syslog severity levels
78
-const (
79
-	LOG_EMERG   = int32(0)
80
-	LOG_ALERT   = int32(1)
81
-	LOG_CRIT    = int32(2)
82
-	LOG_ERR     = int32(3)
83
-	LOG_WARNING = int32(4)
84
-	LOG_NOTICE  = int32(5)
85
-	LOG_INFO    = int32(6)
86
-	LOG_DEBUG   = int32(7)
87
-)
88
-
89
-// numChunks returns the number of GELF chunks necessary to transmit
90
-// the given compressed buffer.
91
-func numChunks(b []byte) int {
92
-	lenB := len(b)
93
-	if lenB <= ChunkSize {
94
-		return 1
95
-	}
96
-	return len(b)/chunkedDataLen + 1
97
-}
98
-
99
-// New returns a new GELF Writer.  This writer can be used to send the
100
-// output of the standard Go log functions to a central GELF server by
101
-// passing it to log.SetOutput()
102
-func NewWriter(addr string) (*Writer, error) {
103
-	var err error
104
-	w := new(Writer)
105
-	w.CompressionLevel = flate.BestSpeed
106
-
107
-	if w.conn, err = net.Dial("udp", addr); err != nil {
108
-		return nil, err
109
-	}
110
-	if w.hostname, err = os.Hostname(); err != nil {
111
-		return nil, err
112
-	}
113
-
114
-	w.Facility = path.Base(os.Args[0])
115
-
116
-	return w, nil
117
-}
118
-
119
-// writes the gzip compressed byte array to the connection as a series
120
-// of GELF chunked messages.  The format is documented at
121
-// http://docs.graylog.org/en/2.1/pages/gelf.html as:
122
-//
123
-//     2-byte magic (0x1e 0x0f), 8 byte id, 1 byte sequence id, 1 byte
124
-//     total, chunk-data
125
-func (w *Writer) writeChunked(zBytes []byte) (err error) {
126
-	b := make([]byte, 0, ChunkSize)
127
-	buf := bytes.NewBuffer(b)
128
-	nChunksI := numChunks(zBytes)
129
-	if nChunksI > 128 {
130
-		return fmt.Errorf("msg too large, would need %d chunks", nChunksI)
131
-	}
132
-	nChunks := uint8(nChunksI)
133
-	// use urandom to get a unique message id
134
-	msgId := make([]byte, 8)
135
-	n, err := io.ReadFull(rand.Reader, msgId)
136
-	if err != nil || n != 8 {
137
-		return fmt.Errorf("rand.Reader: %d/%s", n, err)
138
-	}
139
-
140
-	bytesLeft := len(zBytes)
141
-	for i := uint8(0); i < nChunks; i++ {
142
-		buf.Reset()
143
-		// manually write header.  Don't care about
144
-		// host/network byte order, because the spec only
145
-		// deals in individual bytes.
146
-		buf.Write(magicChunked) //magic
147
-		buf.Write(msgId)
148
-		buf.WriteByte(i)
149
-		buf.WriteByte(nChunks)
150
-		// slice out our chunk from zBytes
151
-		chunkLen := chunkedDataLen
152
-		if chunkLen > bytesLeft {
153
-			chunkLen = bytesLeft
154
-		}
155
-		off := int(i) * chunkedDataLen
156
-		chunk := zBytes[off : off+chunkLen]
157
-		buf.Write(chunk)
158
-
159
-		// write this chunk, and make sure the write was good
160
-		n, err := w.conn.Write(buf.Bytes())
161
-		if err != nil {
162
-			return fmt.Errorf("Write (chunk %d/%d): %s", i,
163
-				nChunks, err)
164
-		}
165
-		if n != len(buf.Bytes()) {
166
-			return fmt.Errorf("Write len: (chunk %d/%d) (%d/%d)",
167
-				i, nChunks, n, len(buf.Bytes()))
168
-		}
169
-
170
-		bytesLeft -= chunkLen
171
-	}
172
-
173
-	if bytesLeft != 0 {
174
-		return fmt.Errorf("error: %d bytes left after sending", bytesLeft)
175
-	}
176
-	return nil
177
-}
178
-
179
-// 1k bytes buffer by default
180
-var bufPool = sync.Pool{
181
-	New: func() interface{} {
182
-		return bytes.NewBuffer(make([]byte, 0, 1024))
183
-	},
184
-}
185
-
186
-func newBuffer() *bytes.Buffer {
187
-	b := bufPool.Get().(*bytes.Buffer)
188
-	if b != nil {
189
-		b.Reset()
190
-		return b
191
-	}
192
-	return bytes.NewBuffer(nil)
193
-}
194
-
195
-// WriteMessage sends the specified message to the GELF server
196
-// specified in the call to New().  It assumes all the fields are
197
-// filled out appropriately.  In general, clients will want to use
198
-// Write, rather than WriteMessage.
199
-func (w *Writer) WriteMessage(m *Message) (err error) {
200
-	mBuf := newBuffer()
201
-	defer bufPool.Put(mBuf)
202
-	if err = m.MarshalJSONBuf(mBuf); err != nil {
203
-		return err
204
-	}
205
-	mBytes := mBuf.Bytes()
206
-
207
-	var (
208
-		zBuf   *bytes.Buffer
209
-		zBytes []byte
210
-	)
211
-
212
-	var zw io.WriteCloser
213
-	switch w.CompressionType {
214
-	case CompressGzip:
215
-		zBuf = newBuffer()
216
-		defer bufPool.Put(zBuf)
217
-		zw, err = gzip.NewWriterLevel(zBuf, w.CompressionLevel)
218
-	case CompressZlib:
219
-		zBuf = newBuffer()
220
-		defer bufPool.Put(zBuf)
221
-		zw, err = zlib.NewWriterLevel(zBuf, w.CompressionLevel)
222
-	case CompressNone:
223
-		zBytes = mBytes
224
-	default:
225
-		panic(fmt.Sprintf("unknown compression type %d",
226
-			w.CompressionType))
227
-	}
228
-	if zw != nil {
229
-		if err != nil {
230
-			return
231
-		}
232
-		if _, err = zw.Write(mBytes); err != nil {
233
-			zw.Close()
234
-			return
235
-		}
236
-		zw.Close()
237
-		zBytes = zBuf.Bytes()
238
-	}
239
-
240
-	if numChunks(zBytes) > 1 {
241
-		return w.writeChunked(zBytes)
242
-	}
243
-	n, err := w.conn.Write(zBytes)
244
-	if err != nil {
245
-		return
246
-	}
247
-	if n != len(zBytes) {
248
-		return fmt.Errorf("bad write (%d/%d)", n, len(zBytes))
249
-	}
250
-
251
-	return nil
20
+type GelfWriter struct {
21
+	addr     string
22
+	conn     net.Conn
23
+	hostname string
24
+	Facility string // defaults to current process name
25
+	proto    string
252 26
 }
253 27
 
254 28
 // Close connection and interrupt blocked Read or Write operations
255
-func (w *Writer) Close() error {
29
+func (w *GelfWriter) Close() error {
256 30
 	return w.conn.Close()
257 31
 }
258
-
259
-/*
260
-func (w *Writer) Alert(m string) (err error)
261
-func (w *Writer) Close() error
262
-func (w *Writer) Crit(m string) (err error)
263
-func (w *Writer) Debug(m string) (err error)
264
-func (w *Writer) Emerg(m string) (err error)
265
-func (w *Writer) Err(m string) (err error)
266
-func (w *Writer) Info(m string) (err error)
267
-func (w *Writer) Notice(m string) (err error)
268
-func (w *Writer) Warning(m string) (err error)
269
-*/
270
-
271
-// getCaller returns the filename and the line info of a function
272
-// further down in the call stack.  Passing 0 in as callDepth would
273
-// return info on the function calling getCallerIgnoringLog, 1 the
274
-// parent function, and so on.  Any suffixes passed to getCaller are
275
-// path fragments like "/pkg/log/log.go", and functions in the call
276
-// stack from that file are ignored.
277
-func getCaller(callDepth int, suffixesToIgnore ...string) (file string, line int) {
278
-	// bump by 1 to ignore the getCaller (this) stackframe
279
-	callDepth++
280
-outer:
281
-	for {
282
-		var ok bool
283
-		_, file, line, ok = runtime.Caller(callDepth)
284
-		if !ok {
285
-			file = "???"
286
-			line = 0
287
-			break
288
-		}
289
-
290
-		for _, s := range suffixesToIgnore {
291
-			if strings.HasSuffix(file, s) {
292
-				callDepth++
293
-				continue outer
294
-			}
295
-		}
296
-		break
297
-	}
298
-	return
299
-}
300
-
301
-func getCallerIgnoringLogMulti(callDepth int) (string, int) {
302
-	// the +1 is to ignore this (getCallerIgnoringLogMulti) frame
303
-	return getCaller(callDepth+1, "/pkg/log/log.go", "/pkg/io/multi.go")
304
-}
305
-
306
-// Write encodes the given string in a GELF message and sends it to
307
-// the server specified in New().
308
-func (w *Writer) Write(p []byte) (n int, err error) {
309
-
310
-	// 1 for the function that called us.
311
-	file, line := getCallerIgnoringLogMulti(1)
312
-
313
-	// remove trailing and leading whitespace
314
-	p = bytes.TrimSpace(p)
315
-
316
-	// If there are newlines in the message, use the first line
317
-	// for the short message and set the full message to the
318
-	// original input.  If the input has no newlines, stick the
319
-	// whole thing in Short.
320
-	short := p
321
-	full := []byte("")
322
-	if i := bytes.IndexRune(p, '\n'); i > 0 {
323
-		short = p[:i]
324
-		full = p
325
-	}
326
-
327
-	m := Message{
328
-		Version:  "1.1",
329
-		Host:     w.hostname,
330
-		Short:    string(short),
331
-		Full:     string(full),
332
-		TimeUnix: float64(time.Now().Unix()),
333
-		Level:    6, // info
334
-		Facility: w.Facility,
335
-		Extra: map[string]interface{}{
336
-			"_file": file,
337
-			"_line": line,
338
-		},
339
-	}
340
-
341
-	if err = w.WriteMessage(&m); err != nil {
342
-		return 0, err
343
-	}
344
-
345
-	return len(p), nil
346
-}
347
-
348
-func (m *Message) MarshalJSONBuf(buf *bytes.Buffer) error {
349
-	b, err := json.Marshal(m)
350
-	if err != nil {
351
-		return err
352
-	}
353
-	// write up until the final }
354
-	if _, err = buf.Write(b[:len(b)-1]); err != nil {
355
-		return err
356
-	}
357
-	if len(m.Extra) > 0 {
358
-		eb, err := json.Marshal(m.Extra)
359
-		if err != nil {
360
-			return err
361
-		}
362
-		// merge serialized message + serialized extra map
363
-		if err = buf.WriteByte(','); err != nil {
364
-			return err
365
-		}
366
-		// write serialized extra bytes, without enclosing quotes
367
-		if _, err = buf.Write(eb[1 : len(eb)-1]); err != nil {
368
-			return err
369
-		}
370
-	}
371
-
372
-	if len(m.RawExtra) > 0 {
373
-		if err := buf.WriteByte(','); err != nil {
374
-			return err
375
-		}
376
-
377
-		// write serialized extra bytes, without enclosing quotes
378
-		if _, err = buf.Write(m.RawExtra[1 : len(m.RawExtra)-1]); err != nil {
379
-			return err
380
-		}
381
-	}
382
-
383
-	// write final closing quotes
384
-	return buf.WriteByte('}')
385
-}
386
-
387
-func (m *Message) UnmarshalJSON(data []byte) error {
388
-	i := make(map[string]interface{}, 16)
389
-	if err := json.Unmarshal(data, &i); err != nil {
390
-		return err
391
-	}
392
-	for k, v := range i {
393
-		if k[0] == '_' {
394
-			if m.Extra == nil {
395
-				m.Extra = make(map[string]interface{}, 1)
396
-			}
397
-			m.Extra[k] = v
398
-			continue
399
-		}
400
-		switch k {
401
-		case "version":
402
-			m.Version = v.(string)
403
-		case "host":
404
-			m.Host = v.(string)
405
-		case "short_message":
406
-			m.Short = v.(string)
407
-		case "full_message":
408
-			m.Full = v.(string)
409
-		case "timestamp":
410
-			m.TimeUnix = v.(float64)
411
-		case "level":
412
-			m.Level = int32(v.(float64))
413
-		case "facility":
414
-			m.Facility = v.(string)
415
-		}
416
-	}
417
-	return nil
418
-}