Add TCP support for GELF log driver
| ... | ... |
@@ -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 |
+} |
| ... | ... |
@@ -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 |
-} |