├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── gelf.go └── gelf_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | gelf 25 | 26 | .DS_Store 27 | *~ 28 | .project 29 | .settings -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | install: 3 | - go get github.com/bmizerany/assert 4 | - go get github.com/lintianzhi/graylogd 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013 Robert Kowalski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/robertkowalski/graylog-golang.png?branch=master)](https://travis-ci.org/robertkowalski/graylog-golang) 2 | 3 | # graylog-golang 4 | 5 | ## graylog-golang is a full implementation for sending messages in GELF (Graylog Extended Log Format) from Go (Golang) to Graylog 6 | 7 | 8 | # Example 9 | 10 | ```go 11 | package main 12 | 13 | import ( 14 | "github.com/robertkowalski/graylog-golang" 15 | ) 16 | 17 | func main() { 18 | 19 | g := gelf.New(gelf.Config{}) 20 | 21 | g.Log(`{ 22 | "version": "1.0", 23 | "host": "localhost", 24 | "timestamp": 1356262644, 25 | "facility": "Google Go", 26 | "short_message": "Hello From Golang!" 27 | }`) 28 | } 29 | ``` 30 | 31 | # Setting Config Values 32 | 33 | ```go 34 | g := gelf.New(gelf.Config{ 35 | GraylogPort: 80, 36 | GraylogHostname: "example.com", 37 | Connection: "wan", 38 | MaxChunkSizeWan: 42, 39 | MaxChunkSizeLan: 1337, 40 | }) 41 | ``` 42 | 43 | # Tests 44 | ``` 45 | go test 46 | ``` 47 | 48 | # Benchmarks 49 | ``` 50 | go test --bench=".*" 51 | ``` 52 | -------------------------------------------------------------------------------- /gelf.go: -------------------------------------------------------------------------------- 1 | package gelf 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "crypto/rand" 7 | "encoding/binary" 8 | "encoding/json" 9 | "errors" 10 | "log" 11 | "math" 12 | "net" 13 | "strconv" 14 | ) 15 | 16 | const ( 17 | defaultGraylogPort = 12201 18 | defaultGraylogHostname = "127.0.0.1" 19 | defaultConnection = "wan" 20 | defaultMaxChunkSizeWan = 1420 21 | defaultMaxChunkSizeLan = 8154 22 | ) 23 | 24 | type Config struct { 25 | GraylogPort int 26 | GraylogHostname string 27 | Connection string 28 | MaxChunkSizeWan int 29 | MaxChunkSizeLan int 30 | } 31 | 32 | type Gelf struct { 33 | Config 34 | } 35 | 36 | func New(config Config) *Gelf { 37 | 38 | if config.GraylogPort == 0 { 39 | config.GraylogPort = defaultGraylogPort 40 | } 41 | if config.GraylogHostname == "" { 42 | config.GraylogHostname = defaultGraylogHostname 43 | } 44 | if config.Connection == "" { 45 | config.Connection = defaultConnection 46 | } 47 | if config.MaxChunkSizeWan == 0 { 48 | config.MaxChunkSizeWan = defaultMaxChunkSizeWan 49 | } 50 | if config.MaxChunkSizeLan == 0 { 51 | config.MaxChunkSizeLan = defaultMaxChunkSizeLan 52 | } 53 | 54 | g := &Gelf{ 55 | Config: config, 56 | } 57 | 58 | return g 59 | } 60 | 61 | func (g *Gelf) Log(message string) { 62 | msgJson := g.ParseJson(message) 63 | 64 | err := g.TestForForbiddenValues(msgJson) 65 | if err != nil { 66 | log.Printf("Uh oh! %s", err) 67 | return 68 | } 69 | 70 | compressed := g.Compress([]byte(message)) 71 | 72 | chunksize := g.Config.MaxChunkSizeWan 73 | length := compressed.Len() 74 | 75 | if length > chunksize { 76 | 77 | chunkCountInt := int(math.Ceil(float64(length) / float64(chunksize))) 78 | 79 | id := make([]byte, 8) 80 | rand.Read(id) 81 | 82 | for i, index := 0, 0; i < length; i, index = i+chunksize, index+1 { 83 | packet := g.CreateChunkedMessage(index, chunkCountInt, id, &compressed) 84 | g.Send(packet.Bytes()) 85 | } 86 | 87 | } else { 88 | g.Send(compressed.Bytes()) 89 | } 90 | } 91 | 92 | func (g *Gelf) CreateChunkedMessage(index int, chunkCountInt int, id []byte, compressed *bytes.Buffer) bytes.Buffer { 93 | var packet bytes.Buffer 94 | 95 | chunksize := g.GetChunksize() 96 | 97 | packet.Write(g.IntToBytes(30)) 98 | packet.Write(g.IntToBytes(15)) 99 | packet.Write(id) 100 | 101 | packet.Write(g.IntToBytes(index)) 102 | packet.Write(g.IntToBytes(chunkCountInt)) 103 | 104 | packet.Write(compressed.Next(chunksize)) 105 | 106 | return packet 107 | } 108 | 109 | func (g *Gelf) GetChunksize() int { 110 | 111 | if g.Config.Connection == "wan" { 112 | return g.Config.MaxChunkSizeWan 113 | } 114 | 115 | if g.Config.Connection == "lan" { 116 | return g.Config.MaxChunkSizeLan 117 | } 118 | 119 | return g.Config.MaxChunkSizeWan 120 | } 121 | 122 | func (g *Gelf) IntToBytes(i int) []byte { 123 | buf := new(bytes.Buffer) 124 | 125 | err := binary.Write(buf, binary.LittleEndian, int8(i)) 126 | if err != nil { 127 | log.Printf("Uh oh! %s", err) 128 | } 129 | return buf.Bytes() 130 | } 131 | 132 | func (g *Gelf) Compress(b []byte) bytes.Buffer { 133 | var buf bytes.Buffer 134 | comp := zlib.NewWriter(&buf) 135 | 136 | comp.Write(b) 137 | comp.Close() 138 | 139 | return buf 140 | } 141 | 142 | func (g *Gelf) ParseJson(msg string) map[string]interface{} { 143 | var i map[string]interface{} 144 | c := []byte(msg) 145 | 146 | json.Unmarshal(c, &i) 147 | 148 | return i 149 | } 150 | 151 | func (g *Gelf) TestForForbiddenValues(gmap map[string]interface{}) error { 152 | if _, err := gmap["_id"]; err { 153 | return errors.New("Key _id is forbidden") 154 | } 155 | 156 | return nil 157 | } 158 | 159 | func (g *Gelf) Send(b []byte) { 160 | var addr = g.Config.GraylogHostname + ":" + strconv.Itoa(g.Config.GraylogPort) 161 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 162 | if err != nil { 163 | log.Printf("Uh oh! %s", err) 164 | return 165 | } 166 | conn, err := net.DialUDP("udp", nil, udpAddr) 167 | if err != nil { 168 | log.Printf("Uh oh! %s", err) 169 | return 170 | } 171 | conn.Write(b) 172 | } 173 | -------------------------------------------------------------------------------- /gelf_test.go: -------------------------------------------------------------------------------- 1 | package gelf 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "net" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/bmizerany/assert" 14 | "github.com/lintianzhi/graylogd" 15 | ) 16 | 17 | var validJson = `{ 18 | "version": "1.0", 19 | "host": "localhost", 20 | "timestamp": "123312312", 21 | "facility": "Google Go", 22 | "short_message": "Hello From Golang! :)" 23 | }` 24 | 25 | var inValidJson = `{ 26 | "_id": "23", 27 | "version": "1.0", 28 | "host": "localhost", 29 | "timestamp": "123312312", 30 | "facility": "Google Go", 31 | "short_message": "Hello From Golang! :)" 32 | }` 33 | 34 | func Benchmark_LogWithShortMessage(b *testing.B) { 35 | b.StopTimer() 36 | g := New(Config{}) 37 | 38 | b.StartTimer() 39 | for i := 0; i < b.N; i++ { 40 | g.Log("Hello World") 41 | } 42 | } 43 | 44 | func Benchmark_LogWithChunks(b *testing.B) { 45 | b.StopTimer() 46 | g := New(Config{ 47 | MaxChunkSizeWan: 10, 48 | MaxChunkSizeLan: 10, 49 | }) 50 | 51 | b.StartTimer() 52 | for i := 0; i < b.N; i++ { 53 | g.Log("sdfsdsdfdsfdsddddfsdfsdsdfdsfdsddddfsdfsdsdfdsfdsddddfsdfsdsdfdsfdsddddfsdfsdsdfdsfdsddddfsdfsdsdfdsfdsddddfsdfsdsdfdsfdsddddfsdfsdsdfdsfdsddddf") 54 | } 55 | } 56 | 57 | func Test_New_itShouldUseDefaultConfigValuesIfNoOtherProvided(t *testing.T) { 58 | g := New(Config{}) 59 | 60 | assert.Equal(t, g.Config.GraylogPort, defaultGraylogPort) 61 | assert.Equal(t, g.Config.GraylogHostname, defaultGraylogHostname) 62 | assert.Equal(t, g.Config.Connection, defaultConnection) 63 | assert.Equal(t, g.Config.MaxChunkSizeWan, defaultMaxChunkSizeWan) 64 | assert.Equal(t, g.Config.MaxChunkSizeLan, defaultMaxChunkSizeLan) 65 | } 66 | 67 | func Test_New_itShouldUseConfigValuesFromArguments(t *testing.T) { 68 | g := New(Config{ 69 | GraylogPort: 80, 70 | GraylogHostname: "foobarhost", 71 | Connection: "wlan", 72 | MaxChunkSizeWan: 42, 73 | MaxChunkSizeLan: 1337, 74 | }) 75 | 76 | assert.Equal(t, g.Config.GraylogPort, 80) 77 | assert.Equal(t, g.Config.GraylogHostname, "foobarhost") 78 | assert.Equal(t, g.Config.Connection, "wlan") 79 | assert.Equal(t, g.Config.MaxChunkSizeWan, 42) 80 | assert.Equal(t, g.Config.MaxChunkSizeLan, 1337) 81 | } 82 | 83 | func Test_ParseJson_itShouldReturnTypeMapStringInterface(t *testing.T) { 84 | g := New(Config{}) 85 | res := g.ParseJson(validJson) 86 | 87 | assert.Equal(t, reflect.TypeOf(res), reflect.TypeOf(make(map[string]interface{}))) 88 | } 89 | 90 | func Test_ParseJson_itShouldParseTheStringToJson(t *testing.T) { 91 | g := New(Config{}) 92 | res := g.ParseJson(validJson) 93 | 94 | assert.Equal(t, res["version"], "1.0") 95 | assert.Equal(t, res["host"], "localhost") 96 | assert.Equal(t, res["timestamp"], "123312312") 97 | assert.Equal(t, res["facility"], "Google Go") 98 | assert.Equal(t, res["short_message"], "Hello From Golang! :)") 99 | } 100 | 101 | func Test_TestForForbiddenValues_itShouldReturnAnErrorIfForbiddenValuesAppear(t *testing.T) { 102 | g := New(Config{}) 103 | res := g.ParseJson(inValidJson) 104 | err := g.TestForForbiddenValues(res) 105 | 106 | assert.NotEqual(t, nil, err) 107 | } 108 | 109 | func Test_TestSend_itShouldSendUdpPacketsToAServer(t *testing.T) { 110 | g := New(Config{ 111 | GraylogPort: 55555, 112 | }) 113 | 114 | done := make(chan int) 115 | go Server(done, 55555, t) 116 | g.Send([]byte("Hello Graylog")) 117 | <-done 118 | } 119 | 120 | func Test_IntToBytes_itShouldCreateBytesFromInts(t *testing.T) { 121 | g := New(Config{}) 122 | 123 | res := g.IntToBytes(20) 124 | expected := make([]int32, 1) 125 | expected[0] = 20 126 | 127 | assert.Equal(t, bytes.Runes(res), expected) 128 | } 129 | 130 | func Test_GetChunksize_itShouldReturnTheValuesForWan(t *testing.T) { 131 | g := New(Config{ 132 | Connection: "wan", 133 | MaxChunkSizeWan: 42, 134 | MaxChunkSizeLan: 1337, 135 | }) 136 | 137 | res := g.GetChunksize() 138 | 139 | assert.Equal(t, 42, res) 140 | } 141 | 142 | func Test_GetChunksize_itShouldReturnTheValuesForLan(t *testing.T) { 143 | g := New(Config{ 144 | Connection: "lan", 145 | MaxChunkSizeWan: 42, 146 | MaxChunkSizeLan: 1337, 147 | }) 148 | 149 | res := g.GetChunksize() 150 | 151 | assert.Equal(t, 1337, res) 152 | } 153 | 154 | func Test_CreateChunkedMessages_itShouldStartWithTheMagicNumber(t *testing.T) { 155 | g := New(Config{}) 156 | b := []byte("message") 157 | buffer := bytes.NewBuffer(b) 158 | 159 | packet := g.CreateChunkedMessage(1, 0, []byte("id"), buffer) 160 | res := packet.String() 161 | 162 | assert.Equal(t, strings.Contains(res, "\x1e\x0f"), true) 163 | } 164 | 165 | func Test_ChunkSize(t *testing.T) { 166 | 167 | waitChan := make(chan bool, 1) 168 | var realB []byte 169 | daeCfg := graylogd.Config{ 170 | ListenAddr: "127.0.0.1:2211", 171 | HandleRaw: func(b []byte) { 172 | assert.Equal(t, realB, b) 173 | waitChan <- true 174 | }, 175 | HandleError: func(addr *net.UDPAddr, err error) { 176 | t.Fatal("should be no error", err) 177 | }, 178 | } 179 | logd, err := graylogd.NewGraylogd(daeCfg) 180 | assert.Equal(t, nil, err) 181 | assert.Equal(t, nil, logd.Run()) 182 | defer logd.Close() 183 | 184 | client := New(Config{ 185 | GraylogPort: 2211, 186 | GraylogHostname: "127.0.0.1", 187 | MaxChunkSizeWan: 1, 188 | MaxChunkSizeLan: 1, 189 | }) 190 | 191 | msgs := []string{ 192 | "11111", 193 | "123jjdd", 194 | } 195 | for _, msg := range msgs { 196 | 197 | realB = []byte(msg) 198 | 199 | client.Log(msg) 200 | select { 201 | case <-waitChan: 202 | case <-time.After(time.Second): 203 | t.Fatal("message is not received") 204 | } 205 | } 206 | } 207 | 208 | func Test_CreateChunkedMessages_itShouldContainAnId(t *testing.T) { 209 | g := New(Config{}) 210 | b := []byte("message") 211 | buffer := bytes.NewBuffer(b) 212 | 213 | packet := g.CreateChunkedMessage(1, 0, []byte("myId"), buffer) 214 | res := packet.String() 215 | 216 | assert.Equal(t, strings.Contains(res, "myId"), true) 217 | } 218 | 219 | func Test_CreateChunkedMessages_itShouldHaveTheIndex(t *testing.T) { 220 | g := New(Config{}) 221 | b := []byte("message") 222 | buffer := bytes.NewBuffer(b) 223 | 224 | packet := g.CreateChunkedMessage(13, 42, []byte("id"), buffer) 225 | 226 | buf := new(bytes.Buffer) 227 | binary.Write(buf, binary.LittleEndian, int8(13)) 228 | 229 | assert.Equal(t, bytes.Contains(packet.Bytes(), buf.Bytes()), true) 230 | } 231 | 232 | func Test_CreateChunkedMessages_itShouldHaveThePacketCount(t *testing.T) { 233 | g := New(Config{}) 234 | b := []byte("message") 235 | buffer := bytes.NewBuffer(b) 236 | 237 | packet := g.CreateChunkedMessage(133, 42, []byte("id"), buffer) 238 | 239 | buf := new(bytes.Buffer) 240 | binary.Write(buf, binary.LittleEndian, int8(42)) 241 | 242 | assert.Equal(t, bytes.Contains(packet.Bytes(), buf.Bytes()), true) 243 | } 244 | 245 | func Server(done chan<- int, port int, t *testing.T) { 246 | laddr, err := net.ResolveUDPAddr("udp", ":"+strconv.Itoa(port)) 247 | if err != nil { 248 | panic(err) 249 | } 250 | buffer := make([]byte, 1024) 251 | for { 252 | conn, err := net.ListenUDP("udp", laddr) 253 | if err != nil { 254 | panic(err) 255 | } 256 | 257 | for { 258 | n, err := conn.Read(buffer) 259 | if err != nil { 260 | panic(err) 261 | } 262 | conn.Close() 263 | if string(buffer[:n]) != "Hello Graylog" { 264 | t.Error("TestServer Error - String not Equal.") 265 | } 266 | done <- 0 267 | return 268 | } 269 | 270 | conn.Close() 271 | } 272 | } 273 | --------------------------------------------------------------------------------