├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── go.mod ├── le.go └── le_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 | *.test 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.12.x 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Gal Ben-Haim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | le_go 2 | ===== 3 | 4 | Golang client library for logentries.com 5 | 6 | It is compatible with http://golang.org/pkg/log/#Logger 7 | and also implements http://golang.org/pkg/io/#Writer 8 | 9 | [![GoDoc](https://godoc.org/github.com/bsphere/le_go?status.png)](https://godoc.org/github.com/bsphere/le_go) 10 | 11 | [![Build Status](https://travis-ci.org/bsphere/le_go.svg)](https://travis-ci.org/bsphere/le_go) 12 | 13 | Usage 14 | ----- 15 | Add a new manual TCP token log at [logentries.com](https://logentries.com/quick-start/) and copy the [token](https://logentries.com/doc/input-token/). 16 | 17 | Installation: `go get github.com/bsphere/le_go` 18 | 19 | **Note:** The Logger is blocking, it can be easily run in a goroutine by calling `go le.Println(...)` 20 | 21 | ```go 22 | package main 23 | 24 | import "github.com/bsphere/le_go" 25 | 26 | func main() { 27 | le, err := le_go.Connect("XXXX-XXXX-XXXX-XXXX") // replace with token 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | defer le.Close() 33 | 34 | le.Println("another test message") 35 | } 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bsphere/le_go 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /le.go: -------------------------------------------------------------------------------- 1 | // Package le_go provides a Golang client library for logging to 2 | // logentries.com over a TCP connection. 3 | // 4 | // it uses an access token for sending log events. 5 | package le_go 6 | 7 | import ( 8 | "crypto/tls" 9 | "fmt" 10 | "net" 11 | "os" 12 | "strings" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | // Logger represents a Logentries logger, 18 | // it holds the open TCP connection, access token, prefix and flags. 19 | // 20 | // all Logger operations are thread safe and blocking, 21 | // log operations can be invoked in a non-blocking way by calling them from 22 | // a goroutine. 23 | type Logger struct { 24 | conn net.Conn 25 | flag int 26 | mu sync.Mutex 27 | prefix string 28 | token string 29 | buf []byte 30 | } 31 | 32 | const lineSep = "\n" 33 | 34 | // Connect creates a new Logger instance and opens a TCP connection to 35 | // logentries.com, 36 | // The token can be generated at logentries.com by adding a new log, 37 | // choosing manual configuration and token based TCP connection. 38 | func Connect(token string) (*Logger, error) { 39 | logger := Logger{ 40 | token: token, 41 | } 42 | 43 | if err := logger.openConnection(); err != nil { 44 | return nil, err 45 | } 46 | 47 | return &logger, nil 48 | } 49 | 50 | // Close closes the TCP connection to logentries.com 51 | func (logger *Logger) Close() error { 52 | if logger.conn != nil { 53 | return logger.conn.Close() 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // Opens a TCP connection to logentries.com 60 | func (logger *Logger) openConnection() error { 61 | conn, err := tls.Dial("tcp", "data.logentries.com:443", &tls.Config{}) 62 | if err != nil { 63 | return err 64 | } 65 | logger.conn = conn 66 | return nil 67 | } 68 | 69 | // It returns if the TCP connection to logentries.com is open 70 | func (logger *Logger) isOpenConnection() bool { 71 | if logger.conn == nil { 72 | return false 73 | } 74 | 75 | buf := make([]byte, 1) 76 | 77 | logger.conn.SetReadDeadline(time.Now()) 78 | 79 | _, err := logger.conn.Read(buf) 80 | 81 | switch err.(type) { 82 | case net.Error: 83 | if err.(net.Error).Timeout() == true { 84 | logger.conn.SetReadDeadline(time.Time{}) 85 | 86 | return true 87 | } 88 | } 89 | 90 | return false 91 | } 92 | 93 | // It ensures that the TCP connection to logentries.com is open. 94 | // If the connection is closed, a new one is opened. 95 | func (logger *Logger) ensureOpenConnection() error { 96 | if !logger.isOpenConnection() { 97 | if err := logger.openConnection(); err != nil { 98 | return err 99 | } 100 | } 101 | 102 | return nil 103 | } 104 | 105 | // Fatal is same as Print() but calls to os.Exit(1) 106 | func (logger *Logger) Fatal(v ...interface{}) { 107 | logger.Output(2, fmt.Sprint(v...)) 108 | os.Exit(1) 109 | } 110 | 111 | // Fatalf is same as Printf() but calls to os.Exit(1) 112 | func (logger *Logger) Fatalf(format string, v ...interface{}) { 113 | logger.Output(2, fmt.Sprintf(format, v...)) 114 | os.Exit(1) 115 | } 116 | 117 | // Fatalln is same as Println() but calls to os.Exit(1) 118 | func (logger *Logger) Fatalln(v ...interface{}) { 119 | logger.Output(2, fmt.Sprintln(v...)) 120 | os.Exit(1) 121 | } 122 | 123 | // Flags returns the logger flags 124 | func (logger *Logger) Flags() int { 125 | return logger.flag 126 | } 127 | 128 | // Output does the actual writing to the TCP connection 129 | func (logger *Logger) Output(calldepth int, s string) error { 130 | var ( 131 | err error 132 | waitPeriod = time.Millisecond 133 | ) 134 | for { 135 | _, err = logger.Write([]byte(s)) 136 | if err != nil { 137 | if connectionErr := logger.openConnection(); connectionErr != nil { 138 | return connectionErr 139 | } 140 | waitPeriod *= 2 141 | time.Sleep(waitPeriod) 142 | continue 143 | } 144 | return err 145 | } 146 | } 147 | 148 | // Panic is same as Print() but calls to panic 149 | func (logger *Logger) Panic(v ...interface{}) { 150 | s := fmt.Sprint(v...) 151 | logger.Output(2, s) 152 | panic(s) 153 | } 154 | 155 | // Panicf is same as Printf() but calls to panic 156 | func (logger *Logger) Panicf(format string, v ...interface{}) { 157 | s := fmt.Sprintf(format, v...) 158 | logger.Output(2, s) 159 | panic(s) 160 | } 161 | 162 | // Panicln is same as Println() but calls to panic 163 | func (logger *Logger) Panicln(v ...interface{}) { 164 | s := fmt.Sprintln(v...) 165 | logger.Output(2, s) 166 | panic(s) 167 | } 168 | 169 | // Prefix returns the logger prefix 170 | func (logger *Logger) Prefix() string { 171 | return logger.prefix 172 | } 173 | 174 | // Print logs a message 175 | func (logger *Logger) Print(v ...interface{}) error { 176 | return logger.Output(2, fmt.Sprint(v...)) 177 | } 178 | 179 | // Printf logs a formatted message 180 | func (logger *Logger) Printf(format string, v ...interface{}) error { 181 | return logger.Output(2, fmt.Sprintf(format, v...)) 182 | } 183 | 184 | // Println logs a message with a linebreak 185 | func (logger *Logger) Println(v ...interface{}) error { 186 | return logger.Output(2, fmt.Sprintln(v...)) 187 | } 188 | 189 | // SetFlags sets the logger flags 190 | func (logger *Logger) SetFlags(flag int) { 191 | logger.flag = flag 192 | } 193 | 194 | // SetPrefix sets the logger prefix 195 | func (logger *Logger) SetPrefix(prefix string) { 196 | logger.prefix = prefix 197 | } 198 | 199 | // Write writes a bytes array to the Logentries TCP connection, 200 | // it adds the access token and prefix and also replaces 201 | // line breaks with the unicode \u2028 character 202 | func (logger *Logger) Write(p []byte) (n int, err error) { 203 | logger.mu.Lock() 204 | if err := logger.ensureOpenConnection(); err != nil { 205 | return 0, err 206 | } 207 | defer logger.mu.Unlock() 208 | 209 | logger.makeBuf(p) 210 | 211 | return logger.conn.Write(logger.buf) 212 | } 213 | 214 | // makeBuf constructs the logger buffer 215 | // it is not safe to be used from within multiple concurrent goroutines 216 | func (logger *Logger) makeBuf(p []byte) { 217 | count := strings.Count(string(p), lineSep) 218 | p = []byte(strings.Replace(string(p), lineSep, "\u2028", count-1)) 219 | 220 | logger.buf = logger.buf[:0] 221 | logger.buf = append(logger.buf, (logger.token + " ")...) 222 | logger.buf = append(logger.buf, (logger.prefix + " ")...) 223 | logger.buf = append(logger.buf, p...) 224 | 225 | if !strings.HasSuffix(string(logger.buf), lineSep) { 226 | logger.buf = append(logger.buf, (lineSep)...) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /le_test.go: -------------------------------------------------------------------------------- 1 | package le_go 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestConnectOpensConnection(t *testing.T) { 11 | le, err := Connect("") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | defer le.Close() 17 | 18 | if le.conn == nil { 19 | t.Fail() 20 | } 21 | 22 | if le.isOpenConnection() == false { 23 | t.Fail() 24 | } 25 | } 26 | 27 | func TestConnectSetsToken(t *testing.T) { 28 | le, err := Connect("myToken") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | defer le.Close() 34 | 35 | if le.token != "myToken" { 36 | t.Fail() 37 | } 38 | } 39 | 40 | func TestCloseClosesConnection(t *testing.T) { 41 | le, err := Connect("") 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | le.Close() 47 | 48 | if le.isOpenConnection() == true { 49 | t.Fail() 50 | } 51 | } 52 | 53 | func TestOpenConnectionOpensConnection(t *testing.T) { 54 | le, err := Connect("") 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | defer le.Close() 60 | 61 | le.openConnection() 62 | 63 | if le.isOpenConnection() == false { 64 | t.Fail() 65 | } 66 | } 67 | 68 | func TestEnsureOpenConnectionDoesNothingOnOpenConnection(t *testing.T) { 69 | le, err := Connect("") 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | defer le.Close() 75 | old := &le.conn 76 | 77 | le.openConnection() 78 | 79 | if old != &le.conn { 80 | t.Fail() 81 | } 82 | } 83 | 84 | func TestEnsureOpenConnectionCreatesNewConnection(t *testing.T) { 85 | le, err := Connect("") 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | defer le.Close() 91 | 92 | le.openConnection() 93 | 94 | if le.isOpenConnection() == false { 95 | t.Fail() 96 | } 97 | } 98 | 99 | func TestFlagsReturnsFlag(t *testing.T) { 100 | le := Logger{flag: 2} 101 | 102 | if le.Flags() != 2 { 103 | t.Fail() 104 | } 105 | } 106 | 107 | func TestSetFlagsSetsFlag(t *testing.T) { 108 | le := Logger{flag: 2} 109 | 110 | le.SetFlags(1) 111 | 112 | if le.flag != 1 { 113 | t.Fail() 114 | } 115 | } 116 | 117 | func TestPrefixReturnsPrefix(t *testing.T) { 118 | le := Logger{prefix: "myPrefix"} 119 | 120 | if le.Prefix() != "myPrefix" { 121 | t.Fail() 122 | } 123 | } 124 | 125 | func TestSetPrefixSetsPrefix(t *testing.T) { 126 | le := Logger{prefix: "myPrefix"} 127 | 128 | le.SetPrefix("myNewPrefix") 129 | 130 | if le.prefix != "myNewPrefix" { 131 | t.Fail() 132 | } 133 | } 134 | 135 | func TestLoggerImplementsWriterInterface(t *testing.T) { 136 | le, err := Connect("myToken") 137 | if err != nil { 138 | t.Fatal(err) 139 | } 140 | 141 | defer le.Close() 142 | 143 | // the test will fail to compile if Logger doesn't implement io.Writer 144 | func(w io.Writer) {}(le) 145 | } 146 | 147 | func TestReplaceNewline(t *testing.T) { 148 | le, err := Connect("myToken") 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | 153 | defer le.Close() 154 | 155 | le.Println("1\n2\n3") 156 | 157 | if strings.Count(string(le.buf), "\u2028") != 2 { 158 | t.Fail() 159 | } 160 | } 161 | 162 | func TestAddNewline(t *testing.T) { 163 | le, err := Connect("myToken") 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | 168 | defer le.Close() 169 | 170 | le.Print("123") 171 | 172 | if !strings.HasSuffix(string(le.buf), "\n") { 173 | t.Fail() 174 | } 175 | 176 | le.Printf("%s", "123") 177 | 178 | if !strings.HasSuffix(string(le.buf), "\n") { 179 | t.Fail() 180 | } 181 | } 182 | 183 | func ExampleLogger() { 184 | le, err := Connect("XXXX-XXXX-XXXX-XXXX") // replace with token 185 | if err != nil { 186 | panic(err) 187 | } 188 | 189 | defer le.Close() 190 | 191 | le.Println("another test message") 192 | } 193 | 194 | func ExampleLogger_write() { 195 | le, err := Connect("XXXX-XXXX-XXXX-XXXX") // replace with token 196 | if err != nil { 197 | panic(err) 198 | } 199 | 200 | defer le.Close() 201 | 202 | fmt.Fprintln(le, "another test message") 203 | } 204 | 205 | func BenchmarkMakeBuf(b *testing.B) { 206 | le := Logger{token: "token"} 207 | 208 | for i := 0; i < b.N; i++ { 209 | le.makeBuf([]byte("test\nstring\n")) 210 | } 211 | } 212 | 213 | func BenchmarkMakeBufWithoutNewlineSuffix(b *testing.B) { 214 | le := Logger{token: "token"} 215 | 216 | for i := 0; i < b.N; i++ { 217 | le.makeBuf([]byte("test\nstring")) 218 | } 219 | } 220 | 221 | func BenchmarkMakeBufWithPrefix(b *testing.B) { 222 | le := Logger{token: "token"} 223 | le.SetPrefix("prefix") 224 | 225 | for i := 0; i < b.N; i++ { 226 | le.makeBuf([]byte("test\nstring\n")) 227 | } 228 | } 229 | --------------------------------------------------------------------------------