├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bench_test.go ├── collector ├── doc.go ├── file.go ├── file_test.go ├── http.go ├── http_test.go ├── internal.go ├── pipeline.go ├── pipeline_test.go ├── socket.go ├── socket_test.go ├── syslog.go ├── syslog_test.go ├── terminal.go ├── terminal_test.go ├── test.crt └── test.key ├── config.go ├── context.go ├── context_test.go ├── doc.go ├── event.go ├── event_test.go ├── example_basic_test.go ├── example_error_reporting_test.go ├── example_features_test.go ├── format ├── bench_test.go ├── buffer.go ├── buffer_test.go ├── doc.go ├── format.go └── format_test.go ├── frame.go ├── frame_test.go ├── hosted ├── doc.go ├── example_loggly_test.go ├── honeybadger.go ├── honeybadger_test.go ├── internal.go ├── loggly.go ├── loggly_test.go ├── opbeat.go ├── opbeat_test.go ├── rollbar.go ├── rollbar_test.go ├── sentry.go ├── sentry_test.go ├── sf_bundle.crt ├── uuid.go └── uuid_test.go ├── internal └── cuetest │ ├── collectors.go │ ├── events.go │ ├── http.go │ ├── json.go │ ├── misc.go │ └── net.go ├── level.go ├── level_test.go ├── logger.go ├── logger_test.go ├── panic.go ├── panic_test.go ├── util_test.go ├── version.go ├── worker.go └── worker_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - 1.4.3 6 | - 1.5.3 7 | - 1.6.0 8 | - tip 9 | 10 | matrix: 11 | allow_failures: 12 | - go: tip 13 | 14 | script: 15 | - go tool -n vet || go get golang.org/x/tools/cmd/vet 16 | - go vet ./... 17 | - go test -v -race ./... 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Cue Changelog 2 | 3 | ## 0.8.0 (2016-03-12) 4 | 5 | - Initial release on Github 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Bob Ziuchkovski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "testing" 25 | "time" 26 | ) 27 | 28 | type noopCollector struct{} 29 | 30 | func (c *noopCollector) Collect(event *Event) error { return nil } 31 | 32 | func BenchmarkUncollected(b *testing.B) { 33 | defer resetCue() 34 | defer b.StopTimer() 35 | 36 | log := NewLogger("test") 37 | b.ResetTimer() 38 | 39 | for n := 0; n < b.N; n++ { 40 | log.Debug("test") 41 | } 42 | } 43 | 44 | func BenchmarkParallelUncollected(b *testing.B) { 45 | defer resetCue() 46 | defer b.StopTimer() 47 | 48 | log := NewLogger("test") 49 | b.ResetTimer() 50 | 51 | b.RunParallel(func(pb *testing.PB) { 52 | for pb.Next() { 53 | log.Debug("test") 54 | } 55 | }) 56 | } 57 | 58 | func BenchmarkSyncNoopCollector(b *testing.B) { 59 | defer resetCue() 60 | defer b.StopTimer() 61 | 62 | c := &noopCollector{} 63 | Collect(DEBUG, c) 64 | 65 | log := NewLogger("test") 66 | b.ResetTimer() 67 | 68 | for n := 0; n < b.N; n++ { 69 | log.Debug("test") 70 | } 71 | } 72 | 73 | func BenchmarkParallelSyncNoopCollector(b *testing.B) { 74 | defer resetCue() 75 | defer b.StopTimer() 76 | 77 | c := &noopCollector{} 78 | Collect(DEBUG, c) 79 | 80 | log := NewLogger("test") 81 | b.ResetTimer() 82 | 83 | b.RunParallel(func(pb *testing.PB) { 84 | for pb.Next() { 85 | log.Debug("test") 86 | } 87 | }) 88 | } 89 | 90 | func BenchmarkAsyncNoopCollector(b *testing.B) { 91 | defer resetCue() 92 | defer b.StopTimer() 93 | 94 | c := &noopCollector{} 95 | CollectAsync(DEBUG, b.N, c) 96 | 97 | log := NewLogger("test") 98 | b.ResetTimer() 99 | 100 | for n := 0; n < b.N; n++ { 101 | log.Debug("test") 102 | } 103 | 104 | err := Close(time.Minute) 105 | if err != nil { 106 | panic(err) 107 | } 108 | } 109 | 110 | func BenchmarkParallelAsyncNoopCollector(b *testing.B) { 111 | defer resetCue() 112 | defer b.StopTimer() 113 | 114 | c := &noopCollector{} 115 | CollectAsync(DEBUG, b.N, c) 116 | 117 | log := NewLogger("test") 118 | b.ResetTimer() 119 | 120 | b.RunParallel(func(pb *testing.PB) { 121 | for pb.Next() { 122 | log.Debug("test") 123 | } 124 | }) 125 | 126 | err := Close(time.Minute) 127 | if err != nil { 128 | panic(err) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /collector/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | /* 22 | Package collector implements event collection. 23 | 24 | Implementations 25 | 26 | This package provides event collection to file, syslog, web servers, and 27 | network sockets. 28 | 29 | Nil Instances 30 | 31 | Collector implementations emit a WARN log event and return a nil collector 32 | instance if required parameters are missing. The cue.Collect and 33 | cue.CollectAsync functions treat nil collectors as a no-op, so this is 34 | perfectly safe. 35 | 36 | Implementing Custom Collectors 37 | 38 | Implementing a new cue.Collector is easy. The Collect method is the only 39 | method in the interface, and cue ensures the method is only called by a 40 | single goroutine. No additional synchronization is required. If the 41 | the collector implements the io.Closer interface, it's Close method will be 42 | called when terminated. See the implementations in this package for examples. 43 | 44 | Collector Failures and Degradation 45 | 46 | Where possible, collector implementations attempt to recover from errors. 47 | The File collector attempts closing and re-opening its file handle, and the 48 | socket and syslog collectors close and open new network connections. Thus 49 | transient errors will recover automatically if the source of the problem is 50 | resolved. However, collectors must still return error values for visibility 51 | and handling by cue workers. 52 | 53 | If a collector returns an error, cue will re-send the event to the collector 54 | 2 additional times before giving up. After the third try, cue puts the 55 | collector into a degraded state and prevents it from collecting new events. 56 | It then emits an error event to all other collectors to surface the 57 | degradation. Finally, it tries sending an error event to the degraded 58 | collector indefinitely until it succeeds, using exponential backoff with a 59 | maximum delay of 5 minutes between attempts. If the collector successfully 60 | sends the error event, the collector is marked healthy again and a WARN event 61 | is emitted to notify other collectors of the returned health. 62 | 63 | If a collector panics, cue recovers the panic, discards the collector, and 64 | emits a FATAL event to other collectors for visibility. 65 | */ 66 | package collector 67 | -------------------------------------------------------------------------------- /collector/file.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue" 26 | "github.com/bobziuchkovski/cue/format" 27 | "os" 28 | "os/signal" 29 | "sync" 30 | "time" 31 | ) 32 | 33 | // File represents configuration for file-based Collector instances. The default 34 | // settings create/append to a file at the given path. File rotation is not 35 | // and will not be supported, but the ReopenSignal and ReopenMissing params 36 | // may be used to coordinate with external log rotators. 37 | type File struct { 38 | // Required 39 | Path string 40 | 41 | // Optional 42 | Flags int // Default: os.O_CREATE | os.O_WRONLY | os.O_APPEND 43 | Perms os.FileMode // Default: 0600 44 | Formatter format.Formatter // Default: format.HumanReadable 45 | 46 | // If set, reopen the file if the specified signal is received. On Unix 47 | // SIGHUP is often used for this purpose. 48 | ReopenSignal os.Signal 49 | 50 | // If set, reopen the file if it's missing. The file path will be checked 51 | // at the time interval specified. 52 | ReopenMissing time.Duration 53 | } 54 | 55 | // New returns a new collector based on the File configuration. 56 | func (f File) New() cue.Collector { 57 | if f.Path == "" { 58 | log.Warn("File.New called to created a collector, but Path param is empty. Returning nil collector.") 59 | return nil 60 | } 61 | if f.Formatter == nil { 62 | f.Formatter = format.HumanReadable 63 | } 64 | if f.Flags == 0 { 65 | f.Flags = os.O_CREATE | os.O_WRONLY | os.O_APPEND 66 | } 67 | if f.Perms == 0 { 68 | f.Perms = 0600 69 | } 70 | 71 | fc := &fileCollector{File: f} 72 | fc.watchSignal() 73 | fc.watchRemoval() 74 | return fc 75 | } 76 | 77 | type fileCollector struct { 78 | File 79 | 80 | mu sync.Mutex 81 | file *os.File 82 | opened bool 83 | } 84 | 85 | func (f *fileCollector) String() string { 86 | return fmt.Sprintf("File(path=%s)", f.Path) 87 | } 88 | 89 | func (f *fileCollector) Collect(event *cue.Event) error { 90 | f.mu.Lock() 91 | defer f.mu.Unlock() 92 | 93 | err := f.ensureOpen() 94 | if err != nil { 95 | f.ensureClosed() 96 | return err 97 | } 98 | 99 | buf := format.GetBuffer() 100 | defer format.ReleaseBuffer(buf) 101 | f.Formatter(buf, event) 102 | 103 | bytes := buf.Bytes() 104 | if bytes[len(bytes)-1] != byte('\n') { 105 | bytes = append(bytes, byte('\n')) 106 | } 107 | _, err = f.file.Write(bytes) 108 | if err != nil { 109 | f.ensureClosed() 110 | } 111 | return err 112 | } 113 | 114 | func (f *fileCollector) Close() error { 115 | f.mu.Lock() 116 | defer f.mu.Unlock() 117 | 118 | if f.file != nil { 119 | return f.file.Close() 120 | } 121 | return nil 122 | } 123 | 124 | func (f *fileCollector) reopen() error { 125 | f.mu.Lock() 126 | defer f.mu.Unlock() 127 | f.ensureClosed() 128 | return f.ensureOpen() 129 | } 130 | 131 | func (f *fileCollector) ensureOpen() error { 132 | if f.file != nil { 133 | return nil 134 | } 135 | 136 | var err error 137 | f.file, err = os.OpenFile(f.Path, f.Flags, f.Perms) 138 | if err == nil { 139 | f.opened = true 140 | } 141 | return err 142 | } 143 | 144 | func (f *fileCollector) ensureClosed() { 145 | if f != nil { 146 | f.file.Close() 147 | f.file = nil 148 | } 149 | f.opened = false 150 | } 151 | 152 | func (f *fileCollector) watchSignal() { 153 | if f.ReopenSignal == nil { 154 | return 155 | } 156 | triggered := make(chan os.Signal, 1) 157 | signal.Notify(triggered, f.ReopenSignal) 158 | 159 | go func() { 160 | for { 161 | <-triggered 162 | f.reopen() 163 | } 164 | }() 165 | } 166 | 167 | func (f *fileCollector) watchRemoval() { 168 | if f.ReopenMissing == 0 { 169 | return 170 | } 171 | go func() { 172 | for { 173 | time.Sleep(f.ReopenMissing) 174 | _, err := os.Stat(f.Path) 175 | if os.IsNotExist(err) { 176 | f.reopen() 177 | } 178 | } 179 | }() 180 | } 181 | -------------------------------------------------------------------------------- /collector/file_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue/format" 26 | "github.com/bobziuchkovski/cue/internal/cuetest" 27 | "io/ioutil" 28 | "os" 29 | "path" 30 | "syscall" 31 | "testing" 32 | "time" 33 | ) 34 | 35 | const fileEventStr = "Jan 2 15:04:00 DEBUG file3.go:3 debug event k1=\"some value\" k2=2 k3=3.5 k4=true\n" 36 | 37 | func TestFileNilCollector(t *testing.T) { 38 | c := File{}.New() 39 | if c != nil { 40 | t.Errorf("Expected a nil collector when the file path is missing, but got %s instead", c) 41 | } 42 | } 43 | 44 | func TestFile(t *testing.T) { 45 | tmp := tmpDir() 46 | defer os.RemoveAll(tmp) 47 | 48 | file := path.Join(tmp, "file") 49 | c := File{Path: file}.New() 50 | c.Collect(cuetest.DebugEvent) 51 | cuetest.CloseCollector(c) 52 | checkFileContents(t, file, fileEventStr) 53 | } 54 | 55 | func TestFileDefaultOptions(t *testing.T) { 56 | tmp := tmpDir() 57 | defer os.RemoveAll(tmp) 58 | 59 | file := path.Join(tmp, "file") 60 | opts := File{Path: file} 61 | 62 | c1 := opts.New() 63 | c1.Collect(cuetest.DebugEvent) 64 | cuetest.CloseCollector(c1) 65 | checkFileContents(t, file, fileEventStr) 66 | 67 | // Check appending 68 | c2 := opts.New() 69 | c2.Collect(cuetest.DebugEvent) 70 | cuetest.CloseCollector(c2) 71 | checkFileContents(t, file, fileEventStr+fileEventStr) 72 | 73 | // Check file mode 74 | stat, err := os.Stat(file) 75 | if err != nil { 76 | t.Errorf("Encountered unexpected error stat'ing file: %s", err) 77 | } 78 | if stat.Mode() != 0600 { 79 | t.Errorf("Expected file mode of %s, but got %s instead", os.FileMode(0600), stat.Mode()) 80 | } 81 | } 82 | 83 | func TestFileExplicitOptions(t *testing.T) { 84 | tmp := tmpDir() 85 | defer os.RemoveAll(tmp) 86 | 87 | file := path.Join(tmp, "file") 88 | opts := File{ 89 | Path: file, 90 | Flags: os.O_CREATE | os.O_WRONLY, 91 | Perms: 0640, 92 | Formatter: format.HumanMessage, 93 | } 94 | 95 | // Ensure custom formatter is used 96 | c1 := opts.New() 97 | c1.Collect(cuetest.DebugEvent) 98 | cuetest.CloseCollector(c1) 99 | checkFileContents(t, file, "debug event k1=\"some value\" k2=2 k3=3.5 k4=true\n") 100 | 101 | // Ensure file is recreated (no append flag specified) 102 | c2 := opts.New() 103 | c2.Collect(cuetest.DebugEvent) 104 | cuetest.CloseCollector(c2) 105 | checkFileContents(t, file, "debug event k1=\"some value\" k2=2 k3=3.5 k4=true\n") 106 | 107 | // Check file mode 108 | stat, err := os.Stat(file) 109 | if err != nil { 110 | t.Errorf("Encountered unexpected error stat'ing file: %s", err) 111 | } 112 | if stat.Mode() != 0640 { 113 | t.Errorf("Expected file mode of %s, but got %s instead", os.FileMode(0640), stat.Mode()) 114 | } 115 | } 116 | 117 | func TestFileReopenOnError(t *testing.T) { 118 | tmp := tmpDir() 119 | defer os.RemoveAll(tmp) 120 | 121 | file := path.Join(tmp, "nonexistant", "file") 122 | opts := File{Path: file} 123 | 124 | c1 := opts.New() 125 | err := c1.Collect(cuetest.DebugEvent) 126 | if err == nil { 127 | t.Error("Expected to receive error when directory doesn't exist for file, but didn't") 128 | } 129 | 130 | err = os.MkdirAll(path.Join(tmp, "nonexistant"), 0700) 131 | if err != nil { 132 | t.Errorf("Encountered unexpected error creating directory: %s", err) 133 | } 134 | 135 | err = c1.Collect(cuetest.DebugEvent) 136 | if err != nil { 137 | t.Errorf("Encountered unexpected error writing to file, even though directory now exists: %s", err) 138 | } 139 | 140 | cuetest.CloseCollector(c1) 141 | checkFileContents(t, file, fileEventStr) 142 | } 143 | 144 | func TestFileReopenSignal(t *testing.T) { 145 | tmp := tmpDir() 146 | defer os.RemoveAll(tmp) 147 | 148 | file := path.Join(tmp, "file") 149 | c := File{ 150 | Path: file, 151 | ReopenSignal: syscall.SIGHUP, 152 | }.New() 153 | c.Collect(cuetest.DebugEvent) 154 | 155 | // Remove the opened log file 156 | err := os.Remove(file) 157 | if err != nil { 158 | t.Errorf("Encountered unexpected error removing file: %s", err) 159 | } 160 | 161 | // Send SIGHUP to ourselves 162 | pid := os.Getpid() 163 | proc, err := os.FindProcess(pid) 164 | if err != nil { 165 | t.Error("Failed to get our pid") 166 | } 167 | proc.Signal(syscall.SIGHUP) 168 | 169 | // Wait for reopen to occurm which will recreate the file 170 | waitExists(file, 5*time.Second) 171 | 172 | c.Collect(cuetest.DebugEvent) 173 | cuetest.CloseCollector(c) 174 | checkFileContents(t, file, fileEventStr) 175 | } 176 | 177 | func TestFileReopenMissing(t *testing.T) { 178 | tmp := tmpDir() 179 | defer os.RemoveAll(tmp) 180 | 181 | file := path.Join(tmp, "file") 182 | c := File{ 183 | Path: file, 184 | ReopenMissing: time.Millisecond, 185 | }.New() 186 | c.Collect(cuetest.DebugEvent) 187 | 188 | err := os.Remove(file) 189 | if err != nil { 190 | t.Errorf("Encountered unexpected error removing file: %s", err) 191 | } 192 | waitExists(file, 5*time.Second) 193 | 194 | c.Collect(cuetest.DebugEvent) 195 | cuetest.CloseCollector(c) 196 | checkFileContents(t, file, fileEventStr) 197 | } 198 | 199 | func TestFileString(t *testing.T) { 200 | tmp := tmpDir() 201 | defer os.RemoveAll(tmp) 202 | 203 | file := path.Join(tmp, "file") 204 | c := File{Path: file}.New() 205 | 206 | // Ensure nothing panics 207 | _ = fmt.Sprint(c) 208 | } 209 | 210 | func tmpDir() string { 211 | dir, err := ioutil.TempDir("", "cue-test") 212 | if err != nil { 213 | panic(err) 214 | } 215 | return dir 216 | } 217 | 218 | func waitExists(path string, timeout time.Duration) { 219 | timer := time.AfterFunc(timeout, func() { 220 | panic("timeout waiting for file to exist") 221 | }) 222 | for { 223 | _, err := os.Stat(path) 224 | if err == nil { 225 | timer.Stop() 226 | return 227 | } 228 | } 229 | } 230 | 231 | func checkFileContents(t *testing.T, path string, expected string) { 232 | bytes, err := ioutil.ReadFile(path) 233 | if err != nil { 234 | t.Errorf("Encountered unexpected error reading file contents: %s", err) 235 | } 236 | 237 | if string(bytes) != expected { 238 | t.Errorf(`File contents don't match expectations 239 | 240 | Expected 241 | ======== 242 | %q 243 | 244 | Received 245 | ======== 246 | %q`, expected, string(bytes)) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /collector/http.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue" 26 | "net/http" 27 | ) 28 | 29 | // HTTP represents configuration for http-based Collector instances. For each 30 | // event, the collector calls RequestFormatter to generate a new http request. 31 | // It then submits the request, setting a cue-specific User-Agent header. The 32 | // response status code is checked, but the content is otherwise ignored. The 33 | // collector treats 4XX and 5XX status codes as errors. 34 | type HTTP struct { 35 | // Required 36 | RequestFormatter func(event *cue.Event) (*http.Request, error) 37 | 38 | // If specified, submit the generated requests via Client 39 | Client *http.Client 40 | } 41 | 42 | // New returns a new collector based on the HTTP configuration. 43 | func (h HTTP) New() cue.Collector { 44 | if h.RequestFormatter == nil { 45 | log.Warn("HTTP.New called to created a collector, but RequestFormatter param is empty. Returning nil collector.") 46 | return nil 47 | } 48 | if h.Client == nil { 49 | h.Client = &http.Client{} 50 | } 51 | return &httpCollector{HTTP: h} 52 | } 53 | 54 | func (h *httpCollector) String() string { 55 | return "HTTP(unknown, please wrap the HTTP collector and implement String())" 56 | } 57 | 58 | type httpCollector struct { 59 | HTTP 60 | } 61 | 62 | func (h *httpCollector) Collect(event *cue.Event) error { 63 | request, err := h.RequestFormatter(event) 64 | if err != nil { 65 | return err 66 | } 67 | request.Header.Set("User-Agent", fmt.Sprintf("github.com/bobziuchkovski/cue %d.%d.%d", cue.Version.Major, cue.Version.Minor, cue.Version.Patch)) 68 | resp, err := h.Client.Do(request) 69 | if resp != nil && resp.Body != nil { 70 | defer resp.Body.Close() 71 | } 72 | if err != nil { 73 | return fmt.Errorf("cue/collector: http error: url=%s, error=%q", request.URL, err.Error()) 74 | } 75 | if resp.StatusCode >= 400 { 76 | return fmt.Errorf("cue/collector: http error: url=%s, code=%d", request.URL, resp.StatusCode) 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /collector/http_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue" 26 | "github.com/bobziuchkovski/cue/format" 27 | "github.com/bobziuchkovski/cue/internal/cuetest" 28 | "io/ioutil" 29 | "net/http" 30 | "net/http/httptest" 31 | "strings" 32 | "testing" 33 | ) 34 | 35 | func TestHTTPNilCollector(t *testing.T) { 36 | c := HTTP{}.New() 37 | if c != nil { 38 | t.Errorf("Expected a nil collector when the http request formatter is missing, but got %s instead", c) 39 | } 40 | } 41 | 42 | func TestHTTP(t *testing.T) { 43 | recorder := cuetest.NewHTTPRequestRecorder() 44 | s := httptest.NewServer(recorder) 45 | defer s.Close() 46 | 47 | c := HTTP{RequestFormatter: newHTTPRequestFormatter(s.URL)}.New() 48 | err := c.Collect(cuetest.DebugEvent) 49 | if err != nil { 50 | t.Errorf("Encountered unexpected error: %s", err) 51 | } 52 | 53 | if len(recorder.Requests()) != 1 { 54 | t.Errorf("Expected exactly 1 request to be sent but saw %d instead", len(recorder.Requests())) 55 | } 56 | checkHTTPRequest(t, recorder.Requests()[0]) 57 | } 58 | 59 | func TestHTTPError(t *testing.T) { 60 | recorder := cuetest.NewHTTPRequestRecorder() 61 | s := httptest.NewServer(recorder) 62 | defer s.Close() 63 | 64 | c := HTTP{ 65 | RequestFormatter: newHTTPRequestFormatter(s.URL), 66 | Client: &http.Client{Transport: cuetest.NewFailingHTTPTransport(1)}, 67 | }.New() 68 | 69 | err := c.Collect(cuetest.DebugEvent) 70 | if err == nil { 71 | t.Error("Expected initial http request to fail, but it didn't") 72 | } 73 | err = c.Collect(cuetest.DebugEvent) 74 | if err != nil { 75 | t.Errorf("Encountered unexpected failure: %s", err) 76 | } 77 | 78 | if len(recorder.Requests()) != 1 { 79 | t.Errorf("Expected exactly 1 request to be sent but saw %d instead", len(recorder.Requests())) 80 | } 81 | checkHTTPRequest(t, recorder.Requests()[0]) 82 | } 83 | 84 | func TestHTTP4XXErrorCode(t *testing.T) { 85 | s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 86 | http.Error(w, "test 400 error", 400) 87 | })) 88 | defer s.Close() 89 | 90 | c := HTTP{RequestFormatter: newHTTPRequestFormatter(s.URL)}.New() 91 | err := c.Collect(cuetest.DebugEvent) 92 | if err == nil { 93 | t.Error("Expected error but didn't receive one") 94 | } 95 | } 96 | 97 | func TestHTTP5XXErrorCode(t *testing.T) { 98 | s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 99 | http.Error(w, "test 500 error", 500) 100 | })) 101 | defer s.Close() 102 | 103 | c := HTTP{RequestFormatter: newHTTPRequestFormatter(s.URL)}.New() 104 | err := c.Collect(cuetest.DebugEvent) 105 | if err == nil { 106 | t.Error("Expected error but didn't receive one") 107 | } 108 | } 109 | 110 | func TestHTTPStirng(t *testing.T) { 111 | c := HTTP{RequestFormatter: newHTTPRequestFormatter("http://bogus.private")}.New() 112 | 113 | // Ensure nothing panics 114 | _ = fmt.Sprint(c) 115 | } 116 | 117 | func checkHTTPRequest(t *testing.T, req *http.Request) { 118 | if req.Method != "POST" { 119 | t.Errorf("Expected POST method but saw %s instead", req.Method) 120 | } 121 | 122 | agentExpectation := fmt.Sprintf("github.com/bobziuchkovski/cue %d.%d.%d", cue.Version.Major, cue.Version.Minor, cue.Version.Patch) 123 | if req.Header.Get("User-Agent") != agentExpectation { 124 | t.Errorf("Expected User-Agent header of %q but saw %q instead", agentExpectation, req.Header.Get("User-Agent")) 125 | } 126 | 127 | body, err := ioutil.ReadAll(req.Body) 128 | if err != nil { 129 | t.Errorf("Encountered unexpected error reading request body: %s", err) 130 | } 131 | 132 | bodyExpectation := "Jan 2 15:04:00 DEBUG file3.go:3 debug event k1=\"some value\" k2=2 k3=3.5 k4=true" 133 | if string(body) != bodyExpectation { 134 | t.Errorf("Expected to receive %q for request body but saw %q instead", bodyExpectation, string(body)) 135 | } 136 | } 137 | 138 | func newHTTPRequestFormatter(url string) func(event *cue.Event) (*http.Request, error) { 139 | return func(event *cue.Event) (*http.Request, error) { 140 | return http.NewRequest("POST", url, strings.NewReader(format.RenderString(format.HumanReadable, event))) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /collector/internal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "github.com/bobziuchkovski/cue" 25 | ) 26 | 27 | var log = cue.NewLogger("github.com/bobziuchkovski/cue/collector") 28 | -------------------------------------------------------------------------------- /collector/pipeline.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue" 26 | "io" 27 | ) 28 | 29 | // ContextFilter is used with a Pipeline to filter context key/value pairs. 30 | type ContextFilter func(key string, value interface{}) bool 31 | 32 | // ContextTransformer is used with a Pipeline to transform context key/value 33 | // pairs. 34 | type ContextTransformer func(context cue.Context) cue.Context 35 | 36 | // EventFilter is used with a Pipeline to filter events. 37 | type EventFilter func(event *cue.Event) bool 38 | 39 | // EventTransformer is used with a Pipeline to transform events. 40 | type EventTransformer func(event *cue.Event) *cue.Event 41 | 42 | // Pipeline is an immutable builder for Event and Context transforms. 43 | // Pipeline methods create and return updated *Pipeline instances. They are 44 | // meant to be invoked as a chain. 45 | // 46 | // Hence the following is correct: 47 | // 48 | // pipe := NewPipeline().FilterContext(...) 49 | // filtered := p.Attach(...) 50 | // 51 | // Whereas the following is incorrect and does nothing: 52 | // 53 | // pipe := NewPipeline() 54 | // pipe.FilterContext(...) // Wrong: the returned *Pipeline is ignored 55 | // filtered := p.Attach(...) 56 | // 57 | // Since pipeline objects are immutable, they may be attached to multiple 58 | // collectors, and may be attached at multiple points during their build 59 | // process to different collectors. 60 | // 61 | // Pipeline passes copies of input events to its filters/transformers, so the 62 | // events may be modified in place. 63 | type Pipeline struct { 64 | prior *Pipeline 65 | transformer EventTransformer 66 | } 67 | 68 | // NewPipeline returns a new pipeline instance. 69 | func NewPipeline() *Pipeline { 70 | return &Pipeline{} 71 | } 72 | 73 | // FilterContext returns an updated copy of Pipeline that drops Context 74 | // key/value pairs that match any of the provided filters. 75 | func (p *Pipeline) FilterContext(filters ...ContextFilter) *Pipeline { 76 | return &Pipeline{ 77 | prior: p, 78 | transformer: filterNilEvent(filterContext(filters...)), 79 | } 80 | } 81 | 82 | // FilterEvent returns an updated copy of Pipeline that drops events 83 | // that match any of the provided filters. 84 | func (p *Pipeline) FilterEvent(filters ...EventFilter) *Pipeline { 85 | return &Pipeline{ 86 | prior: p, 87 | transformer: filterNilEvent(filterEvent(filters...)), 88 | } 89 | } 90 | 91 | // TransformContext returns an updated copy of Pipeline that transforms event 92 | // contexts according to the provided transformers. 93 | func (p *Pipeline) TransformContext(transformers ...ContextTransformer) *Pipeline { 94 | return &Pipeline{ 95 | prior: p, 96 | transformer: filterNilEvent(transformContext(transformers...)), 97 | } 98 | } 99 | 100 | // TransformEvent returns an updated copy of Pipeline that transforms events 101 | // according to the provided transformers. 102 | func (p *Pipeline) TransformEvent(transformers ...EventTransformer) *Pipeline { 103 | return &Pipeline{ 104 | prior: p, 105 | transformer: filterNilEvent(transformEvent(transformers...)), 106 | } 107 | } 108 | 109 | // Attach returns a new collector with the pipeline attached to c. 110 | func (p *Pipeline) Attach(c cue.Collector) cue.Collector { 111 | if p.prior == nil { 112 | log.Warn("Pipeline.Attach called on an empty pipeline.") 113 | } 114 | return &pipelineCollector{ 115 | pipeline: p, 116 | collector: c, 117 | } 118 | } 119 | 120 | func (p *Pipeline) apply(event *cue.Event) *cue.Event { 121 | if event == nil { 122 | return nil 123 | } 124 | if p.prior == nil { 125 | return cloneEvent(event) 126 | } 127 | return p.transformer(p.prior.apply(event)) 128 | } 129 | 130 | type pipelineCollector struct { 131 | pipeline *Pipeline 132 | collector cue.Collector 133 | } 134 | 135 | func (p *pipelineCollector) String() string { 136 | return fmt.Sprintf("Pipeline(target=%s)", p.collector) 137 | } 138 | 139 | func (p *pipelineCollector) Collect(event *cue.Event) error { 140 | transformed := p.pipeline.apply(event) 141 | if transformed == nil { 142 | return nil 143 | } 144 | return p.collector.Collect(transformed) 145 | } 146 | 147 | func (p *pipelineCollector) Close() error { 148 | closer, ok := p.collector.(io.Closer) 149 | if !ok { 150 | return nil 151 | } 152 | return closer.Close() 153 | } 154 | 155 | func filterContext(filters ...ContextFilter) EventTransformer { 156 | return func(event *cue.Event) *cue.Event { 157 | newContext := cue.NewContext(event.Context.Name()) 158 | event.Context.Each(func(key string, value interface{}) { 159 | for _, filter := range filters { 160 | if filter(key, value) { 161 | return 162 | } 163 | } 164 | newContext = newContext.WithValue(key, value) 165 | }) 166 | event.Context = newContext 167 | return event 168 | } 169 | } 170 | 171 | func filterEvent(filters ...EventFilter) EventTransformer { 172 | return func(event *cue.Event) *cue.Event { 173 | for _, filter := range filters { 174 | if filter(event) { 175 | return nil 176 | } 177 | } 178 | return event 179 | } 180 | } 181 | 182 | func transformContext(transformers ...ContextTransformer) EventTransformer { 183 | return func(event *cue.Event) *cue.Event { 184 | for _, trans := range transformers { 185 | event.Context = trans(event.Context) 186 | } 187 | return event 188 | } 189 | } 190 | 191 | func transformEvent(transformers ...EventTransformer) EventTransformer { 192 | return func(event *cue.Event) *cue.Event { 193 | for _, trans := range transformers { 194 | if event == nil { 195 | return nil 196 | } 197 | event = trans(event) 198 | } 199 | return event 200 | } 201 | } 202 | 203 | func filterNilEvent(transformer EventTransformer) EventTransformer { 204 | return func(event *cue.Event) *cue.Event { 205 | if event == nil { 206 | return nil 207 | } 208 | return transformer(event) 209 | } 210 | } 211 | 212 | func cloneEvent(e *cue.Event) *cue.Event { 213 | return &cue.Event{ 214 | Time: e.Time, 215 | Level: e.Level, 216 | Context: e.Context, 217 | Frames: e.Frames, 218 | Error: e.Error, 219 | Message: e.Message, 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /collector/pipeline_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue" 26 | "github.com/bobziuchkovski/cue/internal/cuetest" 27 | "reflect" 28 | "testing" 29 | ) 30 | 31 | func TestPipelineContextFilter(t *testing.T) { 32 | c1 := cuetest.NewCapturingCollector() 33 | p1 := NewPipeline().FilterContext(func(key string, value interface{}) bool { 34 | return key == "k1" 35 | }) 36 | p1.Attach(c1).Collect(cuetest.DebugEvent) 37 | 38 | fieldExpectation := cue.Fields{ 39 | "k2": 2, 40 | "k3": 3.5, 41 | "k4": true, 42 | } 43 | if !reflect.DeepEqual(c1.Captured()[0].Context.Fields(), fieldExpectation) { 44 | t.Errorf("Expected to see altered context %v but saw %v instead", fieldExpectation, c1.Captured()[0].Context.Fields()) 45 | } 46 | 47 | c2 := cuetest.NewCapturingCollector() 48 | p2 := NewPipeline().FilterContext(func(key string, value interface{}) bool { 49 | return key == "bogus" 50 | }) 51 | p2.Attach(c2).Collect(cuetest.DebugEvent) 52 | 53 | if !reflect.DeepEqual(c2.Captured()[0].Context.Fields(), cuetest.DebugEvent.Context.Fields()) { 54 | t.Errorf("Expected to see an unaltered context, but saw %v instead", c2.Captured()[0].Context.Fields()) 55 | } 56 | 57 | if c2.Captured()[0] == cuetest.DebugEvent { 58 | t.Error("Expected to see a cloned event, but saw our same input event instead") 59 | } 60 | } 61 | 62 | func TestPipelineEventFilter(t *testing.T) { 63 | c1 := cuetest.NewCapturingCollector() 64 | p1 := NewPipeline().FilterEvent(func(event *cue.Event) bool { 65 | return event.Level == cue.DEBUG 66 | }) 67 | p1.Attach(c1).Collect(cuetest.DebugEvent) 68 | 69 | if len(c1.Captured()) != 0 { 70 | t.Errorf("Expected to see no events after filtering DEBUG level, but saw %d instead", len(c1.Captured())) 71 | } 72 | 73 | c2 := cuetest.NewCapturingCollector() 74 | p2 := NewPipeline().FilterEvent(func(event *cue.Event) bool { 75 | return event.Level == cue.ERROR 76 | }) 77 | p2.Attach(c2).Collect(cuetest.DebugEvent) 78 | 79 | if len(c2.Captured()) != 1 { 80 | t.Errorf("Expected to a single event after filtering ERROR level, but saw %d instead", len(c2.Captured())) 81 | } 82 | 83 | if c2.Captured()[0] == cuetest.DebugEvent { 84 | t.Error("Expected to see a cloned event, but saw our same input event instead") 85 | } 86 | } 87 | 88 | func TestPipelineContextTransformer(t *testing.T) { 89 | c1 := cuetest.NewCapturingCollector() 90 | p1 := NewPipeline().TransformContext(func(ctx cue.Context) cue.Context { 91 | return cue.NewContext("replaced").WithValue("field", "value") 92 | }) 93 | p1.Attach(c1).Collect(cuetest.DebugEvent) 94 | 95 | if len(c1.Captured()) != 1 { 96 | t.Errorf("Expected to see a single event but saw %d instead", len(c1.Captured())) 97 | } 98 | 99 | capturedCtx := c1.Captured()[0].Context 100 | if capturedCtx.Name() != "replaced" { 101 | t.Errorf("Expected to see context with name %q, not %q", "replaced", capturedCtx.Name()) 102 | } 103 | 104 | if !reflect.DeepEqual(capturedCtx.Fields(), cue.Fields{"field": "value"}) { 105 | t.Errorf("Expected to see context values of %v, not %v", cue.Fields{"field": "value"}, capturedCtx.Fields()) 106 | } 107 | } 108 | 109 | func TestPipelineEventTransformer(t *testing.T) { 110 | c1 := cuetest.NewCapturingCollector() 111 | p1 := NewPipeline().TransformEvent(func(event *cue.Event) *cue.Event { 112 | return cuetest.ErrorEvent 113 | }) 114 | p1.Attach(c1).Collect(cuetest.DebugEvent) 115 | 116 | if len(c1.Captured()) != 1 { 117 | t.Errorf("Expected to see a single event but saw %d instead", len(c1.Captured())) 118 | } 119 | if !reflect.DeepEqual(cuetest.ErrorEvent, c1.Captured()[0]) { 120 | t.Error("Expected to see a copy of errorEVent, but didn't") 121 | } 122 | } 123 | 124 | func TestMultiPipeline(t *testing.T) { 125 | c1 := cuetest.NewCapturingCollector() 126 | p1 := NewPipeline().FilterContext(func(key string, value interface{}) bool { 127 | return key == "k1" 128 | }).FilterEvent(func(event *cue.Event) bool { 129 | return event.Level == cue.ERROR 130 | }).TransformEvent(func(event *cue.Event) *cue.Event { 131 | event.Message = "Replaced message" 132 | return event 133 | }).TransformContext(func(ctx cue.Context) cue.Context { 134 | return ctx.WithValue("addedkey", "addedvalue") 135 | }) 136 | 137 | c2 := p1.Attach(c1) 138 | c2.Collect(cuetest.DebugEvent) 139 | c2.Collect(cuetest.ErrorEvent) 140 | c2.Collect(nil) 141 | cuetest.CloseCollector(c2) 142 | 143 | if len(c1.Captured()) != 1 { 144 | t.Errorf("Expected to see a single event but saw %d instead", len(c1.Captured())) 145 | } 146 | 147 | event := c1.Captured()[0] 148 | fieldExpectation := cue.Fields{ 149 | "k2": 2, 150 | "k3": 3.5, 151 | "k4": true, 152 | "addedkey": "addedvalue", 153 | } 154 | if !reflect.DeepEqual(event.Context.Fields(), fieldExpectation) { 155 | t.Errorf("Expected to see context fields of %v but saw %v instead", fieldExpectation, event.Context.Fields()) 156 | } 157 | if event.Message != "Replaced message" { 158 | t.Errorf("Expected to see message content of %q not %q", "Replaced message", event.Message) 159 | } 160 | } 161 | 162 | func TestPipelineString(t *testing.T) { 163 | c1 := cuetest.NewCapturingCollector() 164 | p1 := NewPipeline().Attach(c1) 165 | 166 | // Ensure nothing panics 167 | _ = fmt.Sprint(p1) 168 | } 169 | -------------------------------------------------------------------------------- /collector/socket.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "crypto/tls" 25 | "fmt" 26 | "github.com/bobziuchkovski/cue" 27 | "github.com/bobziuchkovski/cue/format" 28 | "net" 29 | ) 30 | 31 | // Socket represents configuration for socket-based Collector instances. The 32 | // collector writes messages to a connection specified by the network, address, 33 | // and (optionally) TLS params. The socket connection is opened via net.Dial, 34 | // or by tls.Dial if TLS config is specified. See the net and crypto/tls 35 | // packages for details on supported Network and Address specifications. 36 | type Socket struct { 37 | // Required 38 | Network string 39 | Address string 40 | 41 | // Optional 42 | TLS *tls.Config 43 | Formatter format.Formatter // Default: format.HumanReadable 44 | } 45 | 46 | // New returns a new collector based on the Socket configuration. 47 | func (s Socket) New() cue.Collector { 48 | if s.Network == "" { 49 | log.Warn("Socket.New called to created a collector, but Network param is empty. Returning nil collector.") 50 | return nil 51 | } 52 | if s.Address == "" { 53 | log.Warn("Socket.New called to created a collector, but Address param is empty. Returning nil collector.") 54 | return nil 55 | } 56 | if s.Formatter == nil { 57 | s.Formatter = format.HumanReadable 58 | } 59 | return &socketCollector{Socket: s} 60 | } 61 | 62 | type socketCollector struct { 63 | Socket 64 | conn net.Conn 65 | connected bool 66 | } 67 | 68 | func (s *socketCollector) String() string { 69 | return fmt.Sprintf("Socket(network=%s, address=%s, tls=%t)", s.Network, s.Address, s.TLS != nil) 70 | } 71 | 72 | func (s *socketCollector) Collect(event *cue.Event) error { 73 | if !s.connected { 74 | err := s.reopen() 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | 80 | buf := format.GetBuffer() 81 | defer format.ReleaseBuffer(buf) 82 | s.Formatter(buf, event) 83 | 84 | _, err := s.conn.Write(buf.Bytes()) 85 | if err != nil { 86 | s.conn.Close() 87 | s.conn = nil 88 | s.connected = false 89 | } 90 | return err 91 | } 92 | 93 | func (s *socketCollector) Close() error { 94 | if s.conn != nil { 95 | return s.conn.Close() 96 | } 97 | return nil 98 | } 99 | 100 | func (s *socketCollector) reopen() error { 101 | var err error 102 | if s.TLS != nil { 103 | s.conn, err = tls.Dial(s.Network, s.Address, s.TLS) 104 | if err == nil { 105 | s.connected = true 106 | } 107 | return err 108 | } 109 | s.conn, err = net.Dial(s.Network, s.Address) 110 | if err == nil { 111 | s.connected = true 112 | } 113 | return err 114 | } 115 | -------------------------------------------------------------------------------- /collector/socket_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "crypto/tls" 25 | "fmt" 26 | "github.com/bobziuchkovski/cue/internal/cuetest" 27 | "testing" 28 | ) 29 | 30 | const socketEventStr = "Jan 2 15:04:00 DEBUG file3.go:3 debug event k1=\"some value\" k2=2 k3=3.5 k4=true" 31 | 32 | func TestSocketNilCollector(t *testing.T) { 33 | c := Socket{Address: "bogus"}.New() 34 | if c != nil { 35 | t.Errorf("Expected a nil collector when the socket network is missing, but got %s instead", c) 36 | } 37 | 38 | c = Socket{Network: "bogus"}.New() 39 | if c != nil { 40 | t.Errorf("Expected a nil collector when the socket address is missing, but got %s instead", c) 41 | } 42 | } 43 | 44 | func TestSocketBasic(t *testing.T) { 45 | recorder := cuetest.NewTCPRecorder() 46 | recorder.Start() 47 | defer recorder.Close() 48 | 49 | c := Socket{ 50 | Network: "tcp", 51 | Address: recorder.Address(), 52 | }.New() 53 | 54 | c.Collect(cuetest.DebugEvent) 55 | cuetest.CloseCollector(c) 56 | recorder.CheckStringContents(t, socketEventStr) 57 | } 58 | 59 | func TestSocketTLS(t *testing.T) { 60 | recorder := cuetest.NewTLSRecorder() 61 | recorder.Start() 62 | defer recorder.Close() 63 | 64 | c := Socket{ 65 | Network: "tcp", 66 | Address: recorder.Address(), 67 | TLS: &tls.Config{InsecureSkipVerify: true}, 68 | }.New() 69 | 70 | c.Collect(cuetest.DebugEvent) 71 | cuetest.CloseCollector(c) 72 | recorder.CheckStringContents(t, socketEventStr) 73 | } 74 | 75 | func TestSocketReopenOnError(t *testing.T) { 76 | recorder := cuetest.NewTCPRecorder() 77 | defer recorder.Close() 78 | 79 | c := Socket{ 80 | Network: "tcp", 81 | Address: recorder.Address(), 82 | }.New() 83 | 84 | err := c.Collect(cuetest.DebugEvent) 85 | if err == nil { 86 | t.Error("Expected to see a collector error but didn't") 87 | } 88 | 89 | recorder.Start() 90 | err = c.Collect(cuetest.DebugEvent) 91 | if err != nil { 92 | t.Errorf("Encountered unexpected collector error: %s", err) 93 | } 94 | 95 | cuetest.CloseCollector(c) 96 | recorder.CheckStringContents(t, socketEventStr) 97 | } 98 | 99 | func TestSocketString(t *testing.T) { 100 | recorder := cuetest.NewTCPRecorder() 101 | defer recorder.Close() 102 | 103 | c := Socket{ 104 | Network: "tcp", 105 | Address: recorder.Address(), 106 | }.New() 107 | 108 | // Ensure nothing panics 109 | _ = fmt.Sprint(c) 110 | } 111 | -------------------------------------------------------------------------------- /collector/terminal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "github.com/bobziuchkovski/cue" 25 | "github.com/bobziuchkovski/cue/format" 26 | "os" 27 | ) 28 | 29 | // Terminal represents configuration for stdout/stderr collection. By 30 | // default, all events are logged to stdout. 31 | type Terminal struct { 32 | Formatter format.Formatter // Default: format.HumanReadable 33 | ErrorsToStderr bool // If set, ERROR and FATAL events are written to stderr 34 | } 35 | 36 | // New returns a new collector based on the Terminal configuration. 37 | func (t Terminal) New() cue.Collector { 38 | if t.Formatter == nil { 39 | t.Formatter = format.HumanReadable 40 | } 41 | return &terminalCollector{Terminal: t} 42 | } 43 | 44 | type terminalCollector struct { 45 | Terminal 46 | } 47 | 48 | func (t *terminalCollector) String() string { 49 | return "Terminal()" 50 | } 51 | 52 | func (t *terminalCollector) Collect(event *cue.Event) error { 53 | output := os.Stdout 54 | if t.ErrorsToStderr && (event.Level == cue.ERROR || event.Level == cue.FATAL) { 55 | output = os.Stderr 56 | } 57 | 58 | buf := format.GetBuffer() 59 | defer format.ReleaseBuffer(buf) 60 | t.Formatter(buf, event) 61 | 62 | bytes := buf.Bytes() 63 | if bytes[len(bytes)-1] != byte('\n') { 64 | bytes = append(bytes, byte('\n')) 65 | } 66 | 67 | _, err := output.Write(bytes) 68 | return err 69 | } 70 | -------------------------------------------------------------------------------- /collector/terminal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue/internal/cuetest" 26 | "io/ioutil" 27 | "os" 28 | "testing" 29 | ) 30 | 31 | const terminalDebugStr = "Jan 2 15:04:00 DEBUG file3.go:3 debug event k1=\"some value\" k2=2 k3=3.5 k4=true\n" 32 | const terminalErrorStr = "Jan 2 15:04:00 ERROR file3.go:3 error event: error message k1=\"some value\" k2=2 k3=3.5 k4=true\n" 33 | 34 | func TestTerminal(t *testing.T) { 35 | realStdout, realStderr := os.Stdout, os.Stderr 36 | defer restoreStdoutStderr(realStdout, realStderr) 37 | 38 | stdout, _ := replaceStdoutStderr() 39 | c := Terminal{}.New() 40 | 41 | c.Collect(cuetest.DebugEvent) 42 | c.Collect(cuetest.ErrorEvent) 43 | restoreStdoutStderr(realStdout, realStderr) 44 | 45 | err := stdout.Close() 46 | if err != nil { 47 | t.Errorf("Encountered unexpected error: %s", err) 48 | } 49 | checkFileContents(t, stdout.Name(), terminalDebugStr+terminalErrorStr) 50 | } 51 | 52 | func TestTerminalStderr(t *testing.T) { 53 | realStdout, realStderr := os.Stdout, os.Stderr 54 | defer restoreStdoutStderr(realStdout, realStderr) 55 | 56 | stdout, stderr := replaceStdoutStderr() 57 | c := Terminal{ErrorsToStderr: true}.New() 58 | 59 | c.Collect(cuetest.DebugEvent) 60 | c.Collect(cuetest.ErrorEvent) 61 | restoreStdoutStderr(realStdout, realStderr) 62 | 63 | err := stdout.Close() 64 | if err != nil { 65 | t.Errorf("Encountered unexpected error: %s", err) 66 | } 67 | err = stderr.Close() 68 | if err != nil { 69 | t.Errorf("Encountered unexpected error: %s", err) 70 | } 71 | checkFileContents(t, stdout.Name(), terminalDebugStr) 72 | checkFileContents(t, stderr.Name(), terminalErrorStr) 73 | } 74 | 75 | func TestTerminalString(t *testing.T) { 76 | c := Terminal{ErrorsToStderr: true}.New() 77 | 78 | // Ensure nothing panics 79 | _ = fmt.Sprint(c) 80 | } 81 | 82 | func restoreStdoutStderr(stdout, stderr *os.File) { 83 | os.Stdout = stdout 84 | os.Stderr = stderr 85 | } 86 | 87 | func replaceStdoutStderr() (stdout, stderr *os.File) { 88 | var err error 89 | stdout, err = ioutil.TempFile("", "test-cue") 90 | if err != nil { 91 | panic(err) 92 | } 93 | os.Stdout = stdout 94 | 95 | stderr, err = ioutil.TempFile("", "test-cue") 96 | if err != nil { 97 | panic(err) 98 | } 99 | os.Stderr = stderr 100 | 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /collector/test.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDcjCCAloCCQDqlwmNURRwijANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJV 3 | UzERMA8GA1UECBMIQ29sb3JhZG8xGTAXBgNVBAcTEENvbG9yYWRvIFNwcmluZ3Mx 4 | JjAkBgNVBAoTHWdpdGh1Yi5jb20vYm9ieml1Y2hrb3Zza2kvY3VlMRYwFAYDVQQD 5 | Ew1DdWUgVGVzdCBDZXJ0MB4XDTE2MDMxMTA0MzQ0OVoXDTI2MDMwOTA0MzQ0OVow 6 | ezELMAkGA1UEBhMCVVMxETAPBgNVBAgTCENvbG9yYWRvMRkwFwYDVQQHExBDb2xv 7 | cmFkbyBTcHJpbmdzMSYwJAYDVQQKEx1naXRodWIuY29tL2JvYnppdWNoa292c2tp 8 | L2N1ZTEWMBQGA1UEAxMNQ3VlIFRlc3QgQ2VydDCCASIwDQYJKoZIhvcNAQEBBQAD 9 | ggEPADCCAQoCggEBALJYxaAWQ1iwGjo8VXRisYLVz1rX67kygilvuCB+fqvhZojw 10 | /4urJsuiWexsRL2xnjteuG3GpP7Er6dweXn68Gkv6zmXI8WxGHHSlPmFiym52IF0 11 | Z00TNhXHvkz1JkFZOfrRvAWbsEyl/rrmvK89qSHto/SDoteGHat7TfWiCEtrlf6x 12 | IFLtcQS141mQnlog18y6BYQOWmG9WLzvB2bWadx8AwzFbHwUbRAzVEUWw4WN67u7 13 | KPorjDY7Gc/CBKhUP5VGdfdG3VOPvPY9IjbtmS4wouicoDX2xslv3PWfnq1yZZGc 14 | msPwDJd35/gQ29SaI9OoFXVbBuLbQvN5PfNWm+MCAwEAATANBgkqhkiG9w0BAQUF 15 | AAOCAQEAAy/KgEizAjN6xj23dUFs/p1JDiWrOYYjAeSNnIBGmdRrRww8P0YLg0wi 16 | f8Z1BtoH9zgmSnZZaXKO2POXTS+p2VcrYm8ZQFbIbPX00iJA2fK+KDvzEVJk/FeD 17 | TfSGsiEbmSNAuTKyftBUAdJY7elJYL8YfpAgKIX3PEHKM20ANIXu2XESEqQ9qph8 18 | MbbOxctSM1Slp0sOuk6LSEQNmR5Y7agfUstoEr3L/MWrDpGCOTUVyrLMogRA+Cnc 19 | A25ZV6yG5H/opupAiG+MslSyZ1q7fOz1yCceLKsG9ArFg2NknUkCiudC5ejqs6MT 20 | 6Q60CmbS5W36HXXufkv3jN0nZS+XkQ== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /collector/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAsljFoBZDWLAaOjxVdGKxgtXPWtfruTKCKW+4IH5+q+FmiPD/ 3 | i6smy6JZ7GxEvbGeO164bcak/sSvp3B5efrwaS/rOZcjxbEYcdKU+YWLKbnYgXRn 4 | TRM2Fce+TPUmQVk5+tG8BZuwTKX+uua8rz2pIe2j9IOi14Ydq3tN9aIIS2uV/rEg 5 | Uu1xBLXjWZCeWiDXzLoFhA5aYb1YvO8HZtZp3HwDDMVsfBRtEDNURRbDhY3ru7so 6 | +iuMNjsZz8IEqFQ/lUZ190bdU4+89j0iNu2ZLjCi6JygNfbGyW/c9Z+erXJlkZya 7 | w/AMl3fn+BDb1Joj06gVdVsG4ttC83k981ab4wIDAQABAoIBACIb5EAHwf2WQI3e 8 | uCE4Nubk6XFgVk7nIAm4uihMyQEqbKfIH7eglhzgAf67fjIhZDfKl8827JtlFosZ 9 | ccIogg48AerTwx2uDxTFx4QpTEJAru2jS5ZsFC36M6UYRaa9397eee1Ap2khXiR0 10 | uKVzT4OTpYXAH0bE+auwO2q9AIUbzkG6xKLocQi8ZDWFw0rjbjB8fapvnWFh+nov 11 | Pb5eGQD+xA/ljw22ZBehOdlcAaCF08f2OEvZB/c5RatcfcahysV38ung/nwCL+wo 12 | xvR8tZvxe8PrXVx32t3+Dzxq+2nU6rWPOTgIAoS3JXgeUffEab7vh13xmW1puCyb 13 | JmeMQlECgYEA7Cy5dPwKHl8hI2dvPyA5VwFi5hBEpLMXEUfWhwEOBgMSByR7YP3Q 14 | JkiMZdRw6DuCNEmg3WZHXL1xsIg1gVEFc66NELgeBkSvnKWWRw9PMK9RKrJKsClN 15 | sFPZVSJsdnXSaAY5CDTrvhlgel9lQT7/uR4+FkDdpCFHT9BaWZW4l7cCgYEAwVFa 16 | rfXZj6anpL4B+77vM5dyxkKe3OrqLD+rzdrmWFkxrDKZ4pPYpMXT6quDmuaMjDtd 17 | fINm8lDaJBwALIOjPksTQVgUSSDqv5PqZbqugCy0GBDrlcbdogsJAUJN6qQxz1Ws 18 | jbZ+G1W9Wj+/ILr43pHEjX4Uar3rQJR9ejncZTUCgYBECZ5j1TgVB9LEIEgsJ1xd 19 | dEjJfmZIDE/Y6pkiy2r+0GOhKyFgD76nSL8plsnwHTSlW5C8N3rXfLwD3zmKx4yK 20 | hv6ckm4T4DW3Kvzf+8kUfW0kn7hkh4GaCo3RuNkGR2sWDLThaF2Bpk3k8xZ4dW22 21 | JsA8KPOxFOU2WQ+uPzOugQKBgGQopWXKIe12eBc3xslK9J3zUqj77VkicS950aa3 22 | Sm7tz3mbQfWNikpcoN5N/MKtvpYNT/NqFVVopIze4QwvK83jkddiLihxYI7fsSsB 23 | 3NVV0/1ADv8r6LrDIug/FSWD6ra0edF2gsHg21k9++WWWcjfF0oDz8o6Gf/8r0I7 24 | ZkAdAoGAfPxcdo+aVjqc7Uyq9Y/TCV5JueJSEYEu9gQTfdwQ3fWxRqXCJEzpj2/a 25 | epHZvIW1OlNwjkprZt8nhJwSKpMw1BOxqe9tpfXVTnVdAQ7flYp3AgZawcY57pVM 26 | 24RXejvwdqG80+Ry+h4JEuxOBfzsZ11ZTtNxYtx7PmLv/8MXxdQ= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "sync" 25 | "sync/atomic" 26 | ) 27 | 28 | // cfg holds our global logging config. 29 | var cfg = &atomicConfig{} 30 | 31 | func init() { 32 | cfg.set(newConfig()) 33 | } 34 | 35 | type atomicConfig struct { 36 | // The mutex is only used during config updates. Reads are handled via 37 | // atomic value loads. 38 | mu sync.Mutex 39 | cfg atomic.Value 40 | } 41 | 42 | func (ac *atomicConfig) get() *config { 43 | return ac.cfg.Load().(*config) 44 | } 45 | 46 | func (ac *atomicConfig) set(c *config) { 47 | ac.cfg.Store(c) 48 | } 49 | 50 | func (ac *atomicConfig) lock() { 51 | ac.mu.Lock() 52 | } 53 | 54 | func (ac *atomicConfig) unlock() { 55 | ac.mu.Unlock() 56 | } 57 | 58 | type config struct { 59 | threshold Level 60 | frames int 61 | errorFrames int 62 | registry registry 63 | } 64 | 65 | type registry map[Collector]*entry 66 | 67 | type entry struct { 68 | threshold Level 69 | degraded bool 70 | worker worker 71 | } 72 | 73 | func (e *entry) clone() *entry { 74 | return &entry{ 75 | threshold: e.threshold, 76 | degraded: e.degraded, 77 | worker: e.worker, 78 | } 79 | } 80 | 81 | func newConfig() *config { 82 | return &config{ 83 | threshold: OFF, 84 | frames: 1, 85 | errorFrames: 1, 86 | registry: make(registry), 87 | } 88 | } 89 | 90 | // clone duplicates configuration for atomic updates. 91 | func (c *config) clone() *config { 92 | new := &config{ 93 | threshold: c.threshold, 94 | frames: c.frames, 95 | errorFrames: c.errorFrames, 96 | registry: make(registry), 97 | } 98 | for collector, entry := range c.registry { 99 | new.registry[collector] = entry.clone() 100 | } 101 | return new 102 | } 103 | 104 | // updateThreshold should only be called on a new, cloned config 105 | func (c *config) updateThreshold() { 106 | max := OFF 107 | for _, e := range c.registry { 108 | if e.threshold > max && !e.degraded { 109 | max = e.threshold 110 | } 111 | } 112 | c.threshold = max 113 | } 114 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "fmt" 25 | "reflect" 26 | ) 27 | 28 | var ( 29 | emptyPairs = (*pairs)(nil) 30 | emptyContext = NewContext("") 31 | errorP = (*error)(nil) 32 | errorT = reflect.TypeOf(errorP).Elem() 33 | stringerP = (*fmt.Stringer)(nil) 34 | stringerT = reflect.TypeOf(stringerP).Elem() 35 | ) 36 | 37 | // Fields is a map representation of contextual key/value pairs. 38 | type Fields map[string]interface{} 39 | 40 | // Context is an interface representing contextual key/value pairs. Any 41 | // key/value pair may be added to a context with one exception: an empty string 42 | // is not a valid key. Pointer values are dereferenced and their target is 43 | // added. Values of basic types -- string, bool, integer, float, and complex 44 | // -- are stored directly. Other types, including all slices and arrays, are 45 | // coerced to a string representation via fmt.Sprint. This ensures stored 46 | // context values are immutable. This is important for safe asynchronous 47 | // operation. 48 | // 49 | // Storing duplicate keys is allowed, but the resulting behavior is currently 50 | // undefined. 51 | type Context interface { 52 | // Name returns the name of the context. 53 | Name() string 54 | 55 | // NumValues returns the number of key/value pairs in the Context. 56 | // The counting behavior for duplicate keys is currently undefined. 57 | NumValues() int 58 | 59 | // Each executes function fn on each of the Context's key/value pairs. 60 | // Iteration order is currently undefined. 61 | Each(fn func(key string, value interface{})) 62 | 63 | // Fields returns a map representation of the Context's key/value pairs. 64 | // Duplicate key handling is currently undefined. 65 | Fields() Fields 66 | 67 | // WithFields returns a new Context that adds the key/value pairs from 68 | // fields to the existing key/value pairs. 69 | WithFields(fields Fields) Context 70 | 71 | // WithValue returns a new Context that adds key and value to the existing 72 | // key/value pairs. 73 | WithValue(key string, value interface{}) Context 74 | } 75 | 76 | type context struct { 77 | name string 78 | pairs *pairs 79 | } 80 | 81 | // JoinContext returns a new Context with the given name, containing all the 82 | // key/value pairs joined from the provided contexts. 83 | func JoinContext(name string, contexts ...Context) Context { 84 | // This is pretty inefficient...we could probably create a wrapper view 85 | // that dispatches to the underlying contexts if needed. 86 | joined := NewContext(name) 87 | for _, context := range contexts { 88 | if context == nil { 89 | continue 90 | } 91 | context.Each(func(key string, value interface{}) { 92 | joined = joined.WithValue(key, value) 93 | }) 94 | } 95 | return joined 96 | } 97 | 98 | // NewContext returns a new Context with the given name. 99 | func NewContext(name string) Context { 100 | return &context{ 101 | name: name, 102 | pairs: emptyPairs, 103 | } 104 | } 105 | 106 | func (c *context) String() string { 107 | return fmt.Sprintf("Context(name=%s)", c.name) 108 | } 109 | 110 | func (c *context) Name() string { 111 | return c.name 112 | } 113 | 114 | func (c *context) NumValues() int { 115 | return c.pairs.count() 116 | } 117 | 118 | func (c *context) Each(fn func(key string, value interface{})) { 119 | c.pairs.each(fn) 120 | } 121 | 122 | func (c *context) Fields() Fields { 123 | return c.pairs.toFields() 124 | } 125 | 126 | func (c *context) WithFields(fields Fields) Context { 127 | var new Context = c 128 | for k, v := range fields { 129 | new = new.WithValue(k, v) 130 | } 131 | return new 132 | } 133 | 134 | func (c *context) WithValue(key string, value interface{}) Context { 135 | if key == "" { 136 | return c 137 | } 138 | return &context{ 139 | name: c.name, 140 | pairs: c.pairs.append(key, basicValue(value)), 141 | } 142 | } 143 | 144 | type pairs struct { 145 | prev *pairs 146 | key string 147 | value interface{} 148 | } 149 | 150 | func (p *pairs) append(key string, value interface{}) *pairs { 151 | return &pairs{ 152 | prev: p, 153 | key: key, 154 | value: value, 155 | } 156 | } 157 | 158 | func (p *pairs) each(fn func(key string, value interface{})) { 159 | for current := p; current != nil; current = current.prev { 160 | fn(current.key, current.value) 161 | } 162 | } 163 | 164 | func (p *pairs) count() int { 165 | count := 0 166 | for current := p; current != nil; current = current.prev { 167 | count++ 168 | } 169 | return count 170 | } 171 | 172 | func (p *pairs) toFields() Fields { 173 | if p == nil { 174 | return make(Fields) 175 | } 176 | fields := p.prev.toFields() 177 | fields[p.key] = p.value 178 | return fields 179 | } 180 | 181 | // basicValue serves to dereference pointers and coerce non-basic types to strings, 182 | // ensuring all values are effectively immutable. The latter is critical for 183 | // asynchronous operation. We can't have context values changing while an event is 184 | // queued, or else the logged value won't represent the value as it was at the 185 | // time the event was generated. 186 | func basicValue(value interface{}) interface{} { 187 | rval := reflect.ValueOf(value) 188 | if !rval.IsValid() { 189 | return fmt.Sprint(value) 190 | } 191 | for rval.Kind() == reflect.Ptr { 192 | if rval.IsNil() { 193 | break 194 | } 195 | if rval.Type().Implements(stringerT) || rval.Type().Implements(errorT) { 196 | break 197 | } 198 | rval = rval.Elem() 199 | } 200 | 201 | switch rval.Kind() { 202 | case reflect.Bool, reflect.String: 203 | return rval.Interface() 204 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 205 | return rval.Interface() 206 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 207 | return rval.Interface() 208 | case reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: 209 | return rval.Interface() 210 | default: 211 | return fmt.Sprint(rval.Interface()) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | /* 22 | Package cue implements contextual logging with "batteries included". It has 23 | thorough test coverage and supports logging to stdout/stderr, file, syslog, 24 | and network sockets, as well as hosted third-party logging and error/reporting 25 | services such as Honeybadger, Loggly, Opbeat, Rollbar, and Sentry. 26 | 27 | Cue uses atomic operations to compare logging calls to registered collector 28 | thresholds. This ensures no-op calls are performed quickly and without lock 29 | contention. On a 2015 MacBook Pro, no-op calls take about 16ns/call, meaning 30 | tens of millions of calls may be dispatched per second. Uncollected log calls 31 | are very cheap. 32 | 33 | Furthermore, collector thresholds may be altered dynamically at run-time, on a 34 | per-collector basis. If debugging logs are needed to troubleshoot a live issue, 35 | collector thresholds may be set to the DEBUG level for a short period of time 36 | and then restored to their original levels shortly thereafter. See the SetLevel 37 | function for details. 38 | 39 | Basics 40 | 41 | Logging instances are created via the NewLogger function. A simple convention 42 | is to initialize an unexported package logger: 43 | 44 | var log = cue.NewLogger("some/package/name") 45 | 46 | Additional context information may be added to the package logger via the 47 | log.WithValue and log.WithFields methods: 48 | 49 | func DoSomething(user string) { 50 | log.WithValue("user", user).Info("Doing something") 51 | } 52 | 53 | func DoSomethingElse(user string, authorized bool) { 54 | log.WithFields(cue.Fields{ 55 | "user": user, 56 | "authorized": authorized, 57 | }).Info("Something else requested") 58 | } 59 | 60 | Depending on the collector and log format, output would look something like: 61 | 62 | INFO Something else requested user= authorized= 63 | 64 | Error Logging and Recovery 65 | 66 | Cue simplifies error reporting by logging the given error and message, and then 67 | returning the same error value. Hence you can return the log.Error/log.Errorf 68 | values in-line: 69 | 70 | filename := "somefile" 71 | f, err := os.Create(filename) 72 | if err != nil { 73 | return log.Errorf(err, "Failed to create %q", filename) 74 | } 75 | 76 | Cue provides Collector implementations for popular error reporting services 77 | such as Honeybadger, Rollbar, Sentry, and Opbeat. If one of these collector 78 | implementations were registered, the above code would automatically open a new 79 | error report, complete with stack trace and context information from the logger 80 | instance. See the cue/hosted package for details. 81 | 82 | Finally, cue provides convenience methods for panic and recovery. Calling Panic 83 | or Panicf will log the provided message at the FATAL level and then panic. 84 | Calling Recover recovers from panics and logs the recovered value and message 85 | at the FATAL level. 86 | 87 | func doSomething() { 88 | defer log.Recover("Recovered panic in doSomething") 89 | doSomethingThatPanics() 90 | } 91 | 92 | If a panic is triggered via a cue logger instance's Panic or Panicf methods, 93 | Recover recovers from the panic but only emits the single event from the 94 | Panic/Panicf method. 95 | 96 | Event Collection 97 | 98 | Cue decouples event generation from event collection. Library and framework 99 | authors may generate log events without concern for the details of collection. 100 | Event collection is opt-in -- no collectors are registered by default. 101 | 102 | Event collection, if enabled, should be configured close to a program's main 103 | package/function, not by libraries. This gives the event subscriber complete 104 | control over the behavior of event collection. 105 | 106 | Collectors are registered via the Collect and CollectAsync functions. Each 107 | collector is registered for a given level threshold. The threshold for a 108 | collector may be updated at any time using the SetLevel function. 109 | 110 | Collect registers fully synchronous event collectors. Logging calls that match 111 | a synchronous collector's threshold block until the collector's Collect method 112 | returns successfully. This is dangerous if the Collector performs any 113 | operations that block or return errors. However, it's simple to use and 114 | understand: 115 | 116 | func main() { 117 | // Follow a 12-factor approach and log unbuffered to stdout. 118 | // See http://12factor.net for details. 119 | cue.Collect(cue.INFO, collector.Terminal{}.New()) 120 | defer log.Recover("Recovered from panic in main") 121 | 122 | RunTheProgram() 123 | } 124 | 125 | CollectAsync registers asynchronous collectors. It creates a buffered channel 126 | for the collector and starts a worker goroutine to service events. Logging 127 | calls return after queuing events to the collector channel. If the channel's 128 | buffer is full, the event is dropped and a drop counter is incremented 129 | atomically. This ensures asynchronous logging calls never block. The worker 130 | goroutine detects changes in the atomic drop counter and surfaces drop events 131 | as collector errors. See the cue/collector docs for details on collector 132 | error handling. 133 | 134 | When asynchronous logging is enabled, Close must be called to flush queued 135 | events on program termination. Close is safe to call even if asynchronous 136 | logging isn't enabled -- it returns immediately if no events are queued. 137 | Note that ctrl+c and kill terminate Go programs without triggering 138 | cleanup code. When using asynchronous logging, it's a good idea to register 139 | signal handlers to capture SIGINT (ctrl+c) and SIGTERM (kill ). See the 140 | os/signals package docs for details. 141 | 142 | func main() { 143 | // Use async logging to local syslog 144 | cue.CollectAsync(cue.INFO, 10000, collector.Syslog{ 145 | App: "theapp", 146 | Facility: collector.LOCAL0, 147 | }.New()) 148 | 149 | // Close/flush buffered events on program termination. 150 | // Note that this won't fire if ctrl+c is used or kill . You need 151 | // to install signal handlers for SIGINT/SIGTERM to handle those cases. 152 | defer cue.Close(5 * time.Second) 153 | 154 | defer log.Recover("Recovered from panic in main") 155 | RunTheProgram() 156 | } 157 | 158 | Stack Frame Collection 159 | 160 | By default, cue collects a single stack frame for any event that matches a 161 | registered collector. This ensures collectors may log the file name, package, 162 | and line number for any collected event. SetFrames may be used to alter this 163 | frame count, or disable frame collection entirely. See the SetFrames function 164 | for details. 165 | 166 | When using error reporting services, SetFrames should be used to increase the 167 | errorFrames parameter from the default value of 1 to a value that provides 168 | enough stack context to successfully diagnose reported errors. 169 | */ 170 | package cue 171 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "fmt" 25 | "runtime" 26 | "time" 27 | ) 28 | 29 | // Event represents a log event. A single Event pointer is passed to all 30 | // matching collectors across multiple goroutines. For this reason, Event 31 | // fields -must not- be altered in place. 32 | type Event struct { 33 | Time time.Time // Local time when the event was generated 34 | Level Level // Event severity level 35 | Context Context // Context of the logger that generated the event 36 | Frames []*Frame // Stack frames for the call site, or nil if disabled 37 | Error error // The error associated with the message (ERROR and FATAL levels only) 38 | Message string // The log message 39 | } 40 | 41 | func newEvent(context Context, level Level, cause error, message string) *Event { 42 | now := time.Now() 43 | return &Event{ 44 | Time: now, 45 | Level: level, 46 | Context: context, 47 | Error: cause, 48 | Message: message, 49 | } 50 | } 51 | 52 | func newEventf(context Context, level Level, cause error, format string, values ...interface{}) *Event { 53 | now := time.Now() 54 | return &Event{ 55 | Time: now, 56 | Level: level, 57 | Context: context, 58 | Error: cause, 59 | Message: fmt.Sprintf(format, values...), 60 | } 61 | } 62 | 63 | func (e *Event) captureFrames(skip int, depth int, errorDepth int, recovering bool) { 64 | skip++ 65 | if e.Level == ERROR || e.Level == FATAL { 66 | depth = errorDepth 67 | } 68 | if depth <= 0 { 69 | return 70 | } 71 | 72 | frameFunc := getFrames 73 | if recovering { 74 | frameFunc = getRecoveryFrames 75 | } 76 | frameptrs := frameFunc(skip, depth) 77 | if frameptrs == nil { 78 | return 79 | } 80 | e.Frames = make([]*Frame, len(frameptrs)) 81 | for i, ptr := range frameptrs { 82 | e.Frames[i] = frameForPC(ptr) 83 | } 84 | } 85 | 86 | // Calling panic() adds additional frames to the call stack, so we need to 87 | // find and skip those additional frames. 88 | func getRecoveryFrames(skip int, depth int) []uintptr { 89 | skip++ 90 | panicFrames := getFrames(skip, maxPanicDepth) 91 | for i, pc := range panicFrames { 92 | if frameForPC(pc).Function == "runtime.gopanic" { 93 | return getFrames(skip+i+1, depth) 94 | } 95 | } 96 | 97 | // Couldn't determine the panic frames, so return all the frames, panic 98 | // included. 99 | return getFrames(skip, depth) 100 | } 101 | 102 | func getFrames(skip int, depth int) []uintptr { 103 | skip++ 104 | stack := make([]uintptr, depth) 105 | count := runtime.Callers(skip, stack) 106 | stack = stack[:count] 107 | if count > 0 { 108 | // Per runtime package docs, we need to adjust the pc value in the 109 | // nearest frame to get the actual caller. 110 | stack[0]-- 111 | } 112 | return stack 113 | } 114 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "testing" 25 | ) 26 | 27 | func TestEventSource(t *testing.T) { 28 | e := &Event{} 29 | e.captureFrames(1, 1, 1, false) 30 | if e.Frames[0].Function != "github.com/bobziuchkovski/cue.TestEventSource" { 31 | t.Errorf("Event source function doesn't match expectations. Expected: %s, received: %s", "github.com/bobziuchkovski/cue.TestEventSource", e.Frames[0].Function) 32 | } 33 | } 34 | 35 | func TestEventStack(t *testing.T) { 36 | e := &Event{} 37 | e.captureFrames(1, 2, 2, false) 38 | if e.Frames[0].Function != "github.com/bobziuchkovski/cue.TestEventStack" { 39 | t.Errorf("Event stack[0] function doesn't match expectations. Expected: %s, received: %s", "github.com/bobziuchkovski/cue.TestEventStack", e.Frames[0].Function) 40 | } 41 | if len(e.Frames) != 2 { 42 | t.Errorf("Expected 2 frames but received %d instead", len(e.Frames)) 43 | } 44 | 45 | e2 := &Event{} 46 | if e2.Frames != nil { 47 | t.Error("Expected Event.Frames to return nil when no frames are captured") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example_basic_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Bob Ziuchkovski. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cue_test 6 | 7 | import ( 8 | "github.com/bobziuchkovski/cue" 9 | "github.com/bobziuchkovski/cue/collector" 10 | "os" 11 | "syscall" 12 | ) 13 | 14 | // This example logs to both the terminal (stdout) and to file. 15 | // If the program receives SIGHUP, the file will be reopened (for log rotation). 16 | // Additional context is added via the .WithValue and .WithFields Logger methods. 17 | // 18 | // The formatting may be changed by passing a different formatter to either collector. 19 | // See the cue/format godocs for details. The context data may also be formatted as 20 | // JSON for machine parsing if desired. See cue/format.JSONMessage and cue/format.JSONContext. 21 | func Example_basic() { 22 | cue.Collect(cue.INFO, collector.Terminal{}.New()) 23 | cue.Collect(cue.INFO, collector.File{ 24 | Path: "app.log", 25 | ReopenSignal: syscall.SIGHUP, 26 | }.New()) 27 | 28 | log := cue.NewLogger("example") 29 | log.Debug("Debug message -- a quick no-op since our collector is registered at INFO level") 30 | log.Info("Info message") 31 | log.Warn("Warn message") 32 | 33 | // Add additional context 34 | log.WithValue("items", 2).Infof("This is an %s", "example") 35 | log.WithFields(cue.Fields{ 36 | "user": "bob", 37 | "authenticated": true, 38 | }).Warn("Doing something important") 39 | 40 | host, err := os.Hostname() 41 | if err != nil { 42 | log.Error(err, "Failed to retrieve hostname") 43 | } else { 44 | log.Infof("My hostname is %s", host) 45 | } 46 | 47 | // The output looks something like: 48 | // Mar 13 12:40:10 INFO example_basic_test.go:25 Info message 49 | // Mar 13 12:40:10 WARN example_basic_test.go:26 Warn message 50 | // Mar 13 12:40:10 INFO example_basic_test.go:29 This is an example items=2 51 | // Mar 13 12:40:10 WARN example_basic_test.go:33 Doing something important user=bob authenticated=true 52 | // Mar 13 12:40:10 INFO example_basic_test.go:39 My hostname is pegasus.bobbyz.org 53 | 54 | // The formatting could be changed by passing a different formatter to collector.Terminal. 55 | // see the cue/format docs for details 56 | } 57 | -------------------------------------------------------------------------------- /example_error_reporting_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Bob Ziuchkovski. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cue_test 6 | 7 | import ( 8 | "github.com/bobziuchkovski/cue" 9 | "github.com/bobziuchkovski/cue/hosted" 10 | "os" 11 | "time" 12 | ) 13 | 14 | // This example shows how to use error reporting services. 15 | func Example_errorReporting() { 16 | // Here we're assuming the Honeybadger API key is stored via environment 17 | // variable, as well as an APP_ENV variable specifying "test", "production", etc. 18 | cue.CollectAsync(cue.ERROR, 10000, hosted.Honeybadger{ 19 | Key: os.Getenv("HONEYBADGER_KEY"), 20 | Environment: os.Getenv("APP_ENV"), 21 | }.New()) 22 | 23 | // We want to collect more stack frames for error and panic events so that 24 | // our Honeybadger incidents show us enough stack trace to troubleshoot. 25 | cue.SetFrames(1, 32) 26 | 27 | // We use Close to flush the asynchronous buffer. This way we won't 28 | // lose error reports if one is in the process of sending when the program 29 | // is terminating. 30 | defer cue.Close(5 * time.Second) 31 | 32 | // If something panics, it will automatically open a Honeybadger event 33 | // when recovered by this line 34 | log := cue.NewLogger("example") 35 | defer log.Recover("Recovered panic") 36 | 37 | // Force a panic 38 | PanickingFunc() 39 | } 40 | 41 | func PanickingFunc() { 42 | panic("This will be reported to Honeybadger") 43 | } 44 | -------------------------------------------------------------------------------- /example_features_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Bob Ziuchkovski. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cue_test 6 | 7 | import ( 8 | "github.com/bobziuchkovski/cue" 9 | "github.com/bobziuchkovski/cue/collector" 10 | "github.com/bobziuchkovski/cue/format" 11 | "github.com/bobziuchkovski/cue/hosted" 12 | "os" 13 | "syscall" 14 | "time" 15 | ) 16 | 17 | var log = cue.NewLogger("example") 18 | 19 | // This example shows quite a few of the cue features: logging to a file that 20 | // reopens on SIGHUP (for log rotation), logging colored output to stdout, 21 | // logging to syslog, and reporting errors to Honeybadger. 22 | func Example_features() { 23 | // defer cue.Close before log.Recover so that Close flushes any events 24 | // triggers by panic recovery 25 | defer cue.Close(5 * time.Second) 26 | defer log.Recover("Recovered panic in main") 27 | ConfigureLogging() 28 | RunTheProgram() 29 | } 30 | 31 | func ConfigureLogging() { 32 | // Collect logs to stdout in color! :) 33 | cue.Collect(cue.DEBUG, collector.Terminal{ 34 | Formatter: format.HumanReadableColors, 35 | }.New()) 36 | 37 | // Collect to app.log and reopen the handle if we receive SIGHUP 38 | cue.Collect(cue.INFO, collector.File{ 39 | Path: "app.log", 40 | ReopenSignal: syscall.SIGHUP, 41 | }.New()) 42 | 43 | // Collect to syslog, formatting the context data as JSON for indexing. 44 | cue.Collect(cue.WARN, collector.Syslog{ 45 | App: "app", 46 | Facility: collector.LOCAL7, 47 | Formatter: format.JSONMessage, 48 | }.New()) 49 | 50 | // Report errors asynchronously to Honeybadger. If HONEYBADGER_KEY is 51 | // unset, Honeybadger.New will return nil and cue.CollectAsync will 52 | // ignore it. This works great for development. 53 | cue.CollectAsync(cue.ERROR, 10000, hosted.Honeybadger{ 54 | Key: os.Getenv("HONEYBADGER_KEY"), 55 | Environment: os.Getenv("APP_ENV"), 56 | }.New()) 57 | cue.SetFrames(1, 32) 58 | } 59 | 60 | func RunTheProgram() { 61 | log.Info("Running the program!") 62 | log.WithFields(cue.Fields{ 63 | "sad": true, 64 | "length": 0, 65 | }).Panic("No program", "Whoops, there's no program to run!") 66 | } 67 | -------------------------------------------------------------------------------- /format/bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package format 22 | 23 | import ( 24 | "github.com/bobziuchkovski/cue/internal/cuetest" 25 | "testing" 26 | ) 27 | 28 | func BenchmarkHumanReadable(b *testing.B) { 29 | buf := GetBuffer() 30 | b.ResetTimer() 31 | 32 | for i := 0; i < b.N; i++ { 33 | HumanReadable(buf, cuetest.DebugEvent) 34 | buf.Reset() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /format/buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package format 22 | 23 | import ( 24 | "errors" 25 | "sync" 26 | "unicode/utf8" 27 | ) 28 | 29 | var ( 30 | errBarrier = errors.New("cue/format: write attempted on a buffer that was previously read") 31 | pool = newPool() 32 | ) 33 | 34 | // Using a buffer pool brought basic benchmark runs down from 400016 ns/op to 35 | // 3306 ns/op with a simple test that collected log.Info("test") to a collector 36 | // that applied the HumanReadable format. 37 | type bufferPool struct { 38 | pool *sync.Pool 39 | } 40 | 41 | func newPool() *bufferPool { 42 | return &bufferPool{pool: &sync.Pool{ 43 | New: func() interface{} { 44 | return newBuffer() 45 | }, 46 | }} 47 | } 48 | 49 | func (p *bufferPool) get() Buffer { 50 | buffer := p.pool.Get().(Buffer) 51 | buffer.Reset() 52 | return buffer 53 | } 54 | 55 | func (p *bufferPool) put(b Buffer) { 56 | p.pool.Put(b) 57 | } 58 | 59 | // Buffer represents a simple byte buffer. It's similar to bytes.Buffer but 60 | // with a simpler API and implemented as an interface. 61 | type Buffer interface { 62 | // Bytes returns the buffered bytes. 63 | Bytes() []byte 64 | 65 | // Len Returns the number of buffered bytes. 66 | Len() int 67 | 68 | // Reset restores the buffer to a blank/empty state. The underlying byte 69 | // slice is retained. 70 | Reset() 71 | 72 | // Append appends the byte slice value to the buffer. 73 | Append(value []byte) 74 | 75 | // AppendByte appends the byte value to the buffer. 76 | AppendByte(value byte) 77 | 78 | // AppendRune appends the rune value to the buffer. 79 | AppendRune(value rune) 80 | 81 | // AppendString appends the string value to the buffer. 82 | AppendString(value string) 83 | } 84 | 85 | type buffer struct { 86 | bytes []byte 87 | runebuf [utf8.MaxRune]byte 88 | } 89 | 90 | // GetBuffer returns an empty buffer from a pool of Buffers. A corresponding 91 | // "defer ReleaseBuffer()" should be used to free the buffer when finished. 92 | func GetBuffer() Buffer { 93 | return pool.get() 94 | } 95 | 96 | // ReleaseBuffer returns a buffer to the buffer pool. Failing to release the 97 | // buffer won't cause any harm, as the Go runtime will garbage collect it. 98 | // However, as of Go 1.6, there's a significant performance gain in pooling and 99 | // reusing Buffer instances. 100 | func ReleaseBuffer(buffer Buffer) { 101 | pool.put(buffer) 102 | } 103 | 104 | // newBuffer creates a new buffer instance. Currently, the initialized 105 | // capacity is 64 bytes, but this may change. The buffer grows automatically 106 | // as needed. 107 | func newBuffer() Buffer { 108 | return &buffer{ 109 | bytes: make([]byte, 0, 64), 110 | } 111 | } 112 | 113 | func (b *buffer) Reset() { 114 | b.bytes = b.bytes[:0] 115 | } 116 | 117 | func (b *buffer) Bytes() []byte { 118 | return b.bytes 119 | } 120 | 121 | func (b *buffer) Len() int { 122 | return len(b.bytes) 123 | } 124 | 125 | func (b *buffer) AppendByte(value byte) { 126 | b.ensureCapacity(1) 127 | b.bytes = append(b.bytes, value) 128 | } 129 | 130 | func (b *buffer) AppendRune(value rune) { 131 | if value < utf8.RuneSelf { 132 | b.AppendByte(byte(value)) 133 | return 134 | } 135 | size := utf8.EncodeRune(b.runebuf[:], value) 136 | b.Append(b.runebuf[:size]) 137 | } 138 | 139 | func (b *buffer) AppendString(value string) { 140 | origlen := len(b.bytes) 141 | b.ensureCapacity(len(value)) 142 | b.bytes = b.bytes[:origlen+len(value)] 143 | copy(b.bytes[origlen:], value) 144 | } 145 | 146 | func (b *buffer) Append(value []byte) { 147 | origlen := len(b.bytes) 148 | b.ensureCapacity(len(value)) 149 | b.bytes = b.bytes[:origlen+len(value)] 150 | copy(b.bytes[origlen:], value) 151 | } 152 | 153 | func (b *buffer) ensureCapacity(size int) { 154 | curlen := len(b.bytes) 155 | curcap := cap(b.bytes) 156 | if curlen+size > curcap { 157 | new := make([]byte, curlen, 2*curcap+size) 158 | copy(new, b.bytes) 159 | b.bytes = new 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /format/buffer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package format 22 | 23 | import ( 24 | "strings" 25 | "testing" 26 | ) 27 | 28 | func TestEmptyBuffer(t *testing.T) { 29 | buf := newBuffer() 30 | result := buf.Bytes() 31 | if len(result) != 0 { 32 | t.Errorf("Expected length of new buffer to be 0, not %d", len(result)) 33 | } 34 | } 35 | 36 | func TestBufferWrite(t *testing.T) { 37 | buf := newBuffer() 38 | var slice []byte 39 | for i := 0; i < 255; i++ { 40 | slice = append(slice, byte(i)) 41 | } 42 | buf.Append(slice) 43 | 44 | result := buf.Bytes() 45 | if len(result) != 255 { 46 | t.Errorf("Expected 255 bytes written, but saw %d instead", len(result)) 47 | } 48 | for i, b := range result { 49 | if b != byte(i) { 50 | t.Errorf("Expected byte at offset %d to be 0x%02x but got 0x%02x instead", i, byte(i), b) 51 | } 52 | } 53 | } 54 | 55 | func TestBufferWriteString(t *testing.T) { 56 | buf := newBuffer() 57 | buf.AppendString("hello") 58 | buf.AppendString(" ") 59 | buf.AppendString("world") 60 | if string(buf.Bytes()) != "hello world" { 61 | t.Errorf("Expected buffer contents to be %q, not %q", "hello world", string(buf.Bytes())) 62 | } 63 | 64 | buf = newBuffer() 65 | longstr := strings.Repeat("hello", 1000) 66 | buf.AppendString(longstr) 67 | if string(buf.Bytes()) != longstr { 68 | t.Errorf("Expected buffer contents to be %q, not %q", longstr, string(buf.Bytes())) 69 | } 70 | } 71 | 72 | func TestBufferWriteRune(t *testing.T) { 73 | buf := newBuffer() 74 | buf.AppendRune('日') 75 | buf.AppendRune('本') 76 | if string(buf.Bytes()) != "日本" { 77 | t.Errorf("Expected buffer contents to be %q, not %q", "hello", string(buf.Bytes())) 78 | } 79 | 80 | buf = newBuffer() 81 | for i := 0; i < 1000; i++ { 82 | buf.AppendRune('h') 83 | buf.AppendRune('e') 84 | buf.AppendRune('l') 85 | buf.AppendRune('l') 86 | buf.AppendRune('o') 87 | } 88 | longstr := strings.Repeat("hello", 1000) 89 | if string(buf.Bytes()) != longstr { 90 | t.Errorf("Expected buffer contents to be %q, not %q", "hello", string(buf.Bytes())) 91 | } 92 | } 93 | 94 | func TestWriteByte(t *testing.T) { 95 | buf := newBuffer() 96 | for i := 0; i < 255; i++ { 97 | buf.AppendByte(byte(i)) 98 | } 99 | 100 | result := buf.Bytes() 101 | if len(result) != 255 { 102 | t.Errorf("Expected 255 bytes written, but saw %d instead", len(result)) 103 | } 104 | for i, b := range result { 105 | if b != byte(i) { 106 | t.Errorf("Expected byte at offset %d to be 0x%02x but got 0x%02x instead", i, byte(i), b) 107 | } 108 | } 109 | } 110 | 111 | func TestBufferLen(t *testing.T) { 112 | buf := newBuffer() 113 | for i := 0; i < 255; i++ { 114 | if buf.Len() != i { 115 | t.Errorf("Expected length to equal i (%d), not %d", i, buf.Len()) 116 | } 117 | buf.Append([]byte{byte(i)}) 118 | } 119 | } 120 | 121 | func TestBufferReset(t *testing.T) { 122 | buf := newBuffer() 123 | buf.AppendString("test") 124 | buf.Reset() 125 | if buf.Len() != 0 { 126 | t.Errorf("Buffer should be 0 after reset, but it's %d instead", buf.Len()) 127 | } 128 | if len(buf.Bytes()) != 0 { 129 | t.Errorf("Buffer.Bytes() should have 0 length after reset, but it's %d instead", len(buf.Bytes())) 130 | } 131 | if cap(buf.Bytes()) < 4 { 132 | t.Errorf("Buffer.Bytes() should have capacity greater than or equal to the size of the original buffer (%d), but it's %d instead", len("test"), cap(buf.Bytes())) 133 | } 134 | } 135 | 136 | func TestGetBuffer(t *testing.T) { 137 | buf := GetBuffer() 138 | if buf.Len() != 0 { 139 | t.Errorf("GetBuffer should return a 0 length buffer, but it's %d length instead", buf.Len()) 140 | } 141 | if len(buf.Bytes()) != 0 { 142 | t.Errorf("GetBuffer should return a 0 length buffer, but it's %d instead", len(buf.Bytes())) 143 | } 144 | } 145 | 146 | func TestReleaseBuffer(t *testing.T) { 147 | // Basic test to ensure the release doesn't panic 148 | buf := GetBuffer() 149 | ReleaseBuffer(buf) 150 | } 151 | -------------------------------------------------------------------------------- /format/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | /* 22 | Package format implements event formatting. 23 | 24 | Default Formatters 25 | 26 | The HumanMessage and HumanReadable formats are used as default formatters for 27 | most of the cue/collector and cue/hosted implementations. These are a good 28 | place to start, both for selecting a formatter, and for understanding how to 29 | implement custom formats. 30 | 31 | Custom Formatting 32 | 33 | While the Formatter interface is easy to implement, it's simpler to assemble a 34 | format using the existing formatting functions as building blocks. The Join 35 | and Formatf functions are particularly useful in this regard. Both assemble 36 | a new Formatter based on input formatters. See the predefined formats for 37 | examples. 38 | 39 | Buffers 40 | 41 | All formatters append to a Buffer. The interface is similar to a bytes.Buffer, 42 | but with a simpler API. See the Buffer type documentation for details. 43 | */ 44 | package format 45 | -------------------------------------------------------------------------------- /frame.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "runtime" 25 | "strings" 26 | ) 27 | 28 | // Frame fields use UnknownPackage, UnknownFunction, and UnknownFile when the 29 | // package, function, or file cannot be determined for a stack frame. 30 | const ( 31 | UnknownPackage = "" 32 | UnknownFunction = "" 33 | UnknownFile = "" 34 | ) 35 | 36 | var nilFrame = &Frame{ 37 | Package: UnknownPackage, 38 | Function: UnknownFunction, 39 | File: UnknownFile, 40 | Line: 0, 41 | } 42 | 43 | // Frame represents a single stack frame. 44 | type Frame struct { 45 | Package string // Package name or cue.UnknownPackage ("") if unknown 46 | Function string // Function name or cue.UnknownFunction ("") if unknown 47 | File string // Full file path or cue.UnknownFile ("") if unknown 48 | Line int // Line Number or 0 if unknown 49 | } 50 | 51 | func frameForPC(pc uintptr) *Frame { 52 | fn := runtime.FuncForPC(pc) 53 | if fn == nil { 54 | return nilFrame 55 | } 56 | 57 | file, line := fn.FileLine(pc) 58 | function := fn.Name() 59 | return &Frame{ 60 | Package: packageForFunc(function), 61 | Function: function, 62 | File: file, 63 | Line: line, 64 | } 65 | } 66 | 67 | func packageForFunc(fn string) string { 68 | pkg := fn 69 | slashidx := strings.LastIndex(pkg, "/") 70 | if slashidx == -1 { 71 | slashidx = 0 72 | } 73 | dotidx := strings.Index(pkg[slashidx:], ".") 74 | if dotidx == -1 { 75 | dotidx = len(pkg) 76 | } 77 | return pkg[:slashidx+dotidx] 78 | } 79 | -------------------------------------------------------------------------------- /frame_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "runtime" 25 | "strings" 26 | "testing" 27 | ) 28 | 29 | // This test should be first since it's sensitive to it's own position in the 30 | // source file. 31 | func TestFrameLine(t *testing.T) { 32 | // The line number we capture/compare is for the next line 33 | pc, _, _, ok := runtime.Caller(0) 34 | if !ok { 35 | t.Error("Failed to get current stack pointer") 36 | } 37 | frame := frameForPC(pc) 38 | if frame.Line != 33 { 39 | t.Errorf("Expected line number 33 but received %d instead", frame.Line) 40 | } 41 | } 42 | 43 | func TestFrameFile(t *testing.T) { 44 | pc, _, _, ok := runtime.Caller(0) 45 | if !ok { 46 | t.Error("Failed to get current stack pointer") 47 | } 48 | frame := frameForPC(pc) 49 | if !strings.HasSuffix(frame.File, "github.com/bobziuchkovski/cue/frame_test.go") { 50 | t.Errorf("Expected frame.File() to have suffix with current file name, but it didn't. frame.File: %s", frame.File) 51 | } 52 | } 53 | 54 | func TestFrameFunction(t *testing.T) { 55 | pc, _, _, ok := runtime.Caller(0) 56 | if !ok { 57 | t.Error("Failed to get current stack pointer") 58 | } 59 | frame := frameForPC(pc) 60 | if frame.Function != "github.com/bobziuchkovski/cue.TestFrameFunction" { 61 | t.Errorf("Frame function is incorrect. Expected: %s, Received: %s", "github.com/bobziuchkovski/cue.TestFrameFunction", frame.Function) 62 | } 63 | } 64 | 65 | func TestFramePackage(t *testing.T) { 66 | pc, _, _, ok := runtime.Caller(0) 67 | if !ok { 68 | t.Error("Failed to get current stack pointer") 69 | } 70 | frame := frameForPC(pc) 71 | if frame.Package != "github.com/bobziuchkovski/cue" { 72 | t.Errorf("Frame package is incorrect. Expected: %s, Received: %s", "github.com/bobziuchkovski/cue", frame.Package) 73 | } 74 | } 75 | 76 | func TestNilFrame(t *testing.T) { 77 | frame := frameForPC(0) 78 | if frame.File != UnknownFile { 79 | t.Errorf("Expected Frame.File to return %q when frame is unknown", UnknownFile) 80 | } 81 | if frame.Function != UnknownFunction { 82 | t.Errorf("Expected Frame.Function to return %q when frame is unknown", UnknownFunction) 83 | } 84 | if frame.Line != 0 { 85 | t.Error("Expected Frame.Line to return 0 when frame is unknown") 86 | } 87 | if frame.Package != UnknownPackage { 88 | t.Errorf("Expected Frame.Package to return %q when frame is unknown", UnknownPackage) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /hosted/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | /* 22 | Package hosted implements event collection for hosted third-party services. 23 | Collectors are provided for Honeybadger, Loggly, Opbeat, Rollbar, and Sentry. 24 | Additional collectors will be added upon request. 25 | 26 | Inclusion Criteria 27 | 28 | The following criteria are used to evaluate third-party services: 29 | 30 | 1. Does the service provide a perpetual free tier? This is a must-have 31 | requirement. 32 | 33 | 2. Does the service offer transport security? This is a must-have 34 | requirement. 35 | 36 | 3. Is the service a good fit for collecting cue events? Logging and error 37 | reporting services are a great fit. E-mail and messaging services, on the 38 | other hand, are intentionally omitted. 39 | 40 | 4. Does the service provide a sane API/integration mechanism? Firing JSON 41 | HTTP posts is simple, whereas implementing proprietary transport mechanisms 42 | is a pain. 43 | 44 | If a third-party service meets the above criteria and isn't supported, feel 45 | free to open a feature request. 46 | 47 | Frame Collection 48 | 49 | By default, cue collects a single stack frame for all logged events. 50 | Increasing the number of frames collected for ERROR and FATAL events is a 51 | good idea when using error reporting services. See the cue.SetFrames docs for 52 | details. 53 | 54 | Nil Instances 55 | 56 | Collector implementations emit a WARN log event and return a nil collector 57 | instance if required parameters are missing. The cue.Collect and 58 | cue.CollectAsync functions treat nil collectors as a no-op, so this is 59 | perfectly safe. 60 | */ 61 | package hosted 62 | -------------------------------------------------------------------------------- /hosted/example_loggly_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Bob Ziuchkovski. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package hosted_test 6 | 7 | import ( 8 | "crypto/tls" 9 | "crypto/x509" 10 | "github.com/bobziuchkovski/cue" 11 | "github.com/bobziuchkovski/cue/collector" 12 | "github.com/bobziuchkovski/cue/hosted" 13 | "io/ioutil" 14 | "os" 15 | ) 16 | 17 | // This example demonstrates how to use Loggly with TLS transport encryption 18 | // enabled. 19 | func ExampleLoggly_transportEncryption() { 20 | // Load Loggly's CA cert. Please see the Loggly docs for details on 21 | // retrieving their cert files. 22 | pem, err := ioutil.ReadFile("sf_bundle.crt") 23 | if err != nil { 24 | panic(err) 25 | } 26 | cacert := x509.NewCertPool() 27 | if !cacert.AppendCertsFromPEM(pem) { 28 | panic("failed to load loggly CA cert") 29 | } 30 | 31 | cue.Collect(cue.INFO, hosted.Loggly{ 32 | Token: os.Getenv("LOGGLY_TOKEN"), 33 | App: "example_loggly_tls", 34 | Facility: collector.LOCAL0, 35 | Network: "tcp", 36 | Address: "logs-01.loggly.com:6514", // Loggly uses port 6514 for TLS 37 | TLS: &tls.Config{RootCAs: cacert}, 38 | }.New()) 39 | 40 | log := cue.NewLogger("example") 41 | log.Info("This event is sent over TLS") 42 | } 43 | -------------------------------------------------------------------------------- /hosted/honeybadger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "bytes" 25 | "encoding/json" 26 | "fmt" 27 | "github.com/bobziuchkovski/cue" 28 | "github.com/bobziuchkovski/cue/collector" 29 | "github.com/bobziuchkovski/cue/format" 30 | "net/http" 31 | ) 32 | 33 | // Honeybadger represents configuration for the Honeybadger service. Collected 34 | // events are sent to Honeybadger as new error occurrences, complete with 35 | // relevant stack trace. Honeybadger only supports error/fatal events, so 36 | // collectors for the service should only be registered at the ERROR or FATAL 37 | // log levels. 38 | type Honeybadger struct { 39 | // Required 40 | Key string // Honeybadger API key 41 | 42 | // Optional 43 | Tags []string // Tags to send with every event 44 | ExtraContext cue.Context // Additional context values to send with every event 45 | Environment string // Environment name ("development", "production", etc.) 46 | } 47 | 48 | // New returns a new collector based on the Honeybadger configuration. 49 | func (h Honeybadger) New() cue.Collector { 50 | if h.Key == "" { 51 | log.Warn("Honeybadger.New called to created a collector, but Key param is empty. Returning nil collector.") 52 | return nil 53 | } 54 | return &honeybadgerCollector{ 55 | Honeybadger: h, 56 | http: collector.HTTP{RequestFormatter: h.formatRequest}.New(), 57 | } 58 | } 59 | 60 | func (h Honeybadger) formatRequest(event *cue.Event) (request *http.Request, err error) { 61 | body := format.RenderBytes(h.formatBody, event) 62 | request, err = http.NewRequest("POST", "https://api.honeybadger.io/v1/notices", bytes.NewReader(body)) 63 | if err != nil { 64 | return 65 | } 66 | request.Header.Set("Accept", "application/json") 67 | request.Header.Set("Content-Type", "application/json") 68 | request.Header.Set("X-API-Key", h.Key) 69 | return 70 | } 71 | 72 | func (h Honeybadger) formatBody(buffer format.Buffer, event *cue.Event) { 73 | post := &honeybadgerPost{ 74 | Error: h.errorFor(event), 75 | Request: h.requestFor(event), 76 | Server: h.server(), 77 | Notifier: honeybadgerNotifier{ 78 | Name: "github.com/bobziuchkovski/cue", 79 | URL: "https://github.com/bobziuchkovski/cue", 80 | Version: fmt.Sprintf("%d.%d.%d", cue.Version.Major, cue.Version.Minor, cue.Version.Patch), 81 | }, 82 | } 83 | marshalled, _ := json.Marshal(post) 84 | buffer.Append(marshalled) 85 | } 86 | 87 | func (h Honeybadger) requestFor(event *cue.Event) honeybadgerRequest { 88 | pkg := "" 89 | if len(event.Frames) > 0 && event.Frames[0].Package != cue.UnknownPackage { 90 | pkg = event.Frames[0].Package 91 | } 92 | return honeybadgerRequest{ 93 | Context: cue.JoinContext("", event.Context, h.ExtraContext).Fields(), 94 | Component: pkg, 95 | } 96 | } 97 | 98 | func (h Honeybadger) errorFor(event *cue.Event) honeybadgerError { 99 | return honeybadgerError{ 100 | Class: format.RenderString(format.ErrorType, event), 101 | Message: format.RenderString(format.MessageWithError, event), 102 | Tags: h.Tags, 103 | Backtrace: h.backtraceFor(event), 104 | } 105 | } 106 | 107 | func (h Honeybadger) backtraceFor(event *cue.Event) []*honeybadgerFrame { 108 | var backtrace []*honeybadgerFrame 109 | for _, frame := range event.Frames { 110 | backtrace = append(backtrace, &honeybadgerFrame{ 111 | Number: frame.Line, 112 | File: frame.File, 113 | Method: frame.Function, 114 | }) 115 | } 116 | return backtrace 117 | } 118 | 119 | func (h Honeybadger) server() honeybadgerServer { 120 | return honeybadgerServer{ 121 | EnvironmentName: h.Environment, 122 | Hostname: format.RenderString(format.FQDN, nil), 123 | } 124 | } 125 | 126 | type honeybadgerCollector struct { 127 | Honeybadger 128 | http cue.Collector 129 | } 130 | 131 | func (h *honeybadgerCollector) String() string { 132 | return fmt.Sprintf("Honeybadger(environment=%q)", h.Environment) 133 | } 134 | 135 | func (h *honeybadgerCollector) Collect(event *cue.Event) error { 136 | return h.http.Collect(event) 137 | } 138 | 139 | type honeybadgerPost struct { 140 | Notifier honeybadgerNotifier `json:"notifier"` 141 | Error honeybadgerError `json:"error"` 142 | Request honeybadgerRequest `json:"request"` 143 | Server honeybadgerServer `json:"server"` 144 | } 145 | 146 | type honeybadgerError struct { 147 | Class string `json:"class"` 148 | Message string `json:"message"` 149 | Tags []string `json:"tags,omitempty"` 150 | Backtrace []*honeybadgerFrame `json:"backtrace,omitempty"` 151 | } 152 | 153 | type honeybadgerFrame struct { 154 | Number int `json:"number"` 155 | File string `json:"file"` 156 | Method string `json:"method"` 157 | } 158 | 159 | type honeybadgerRequest struct { 160 | Context cue.Fields `json:"context"` 161 | Component string `json:"component,omitempty"` 162 | } 163 | 164 | type honeybadgerNotifier struct { 165 | Name string `json:"name"` 166 | URL string `json:"url"` 167 | Version string `json:"version"` 168 | } 169 | 170 | type honeybadgerServer struct { 171 | EnvironmentName string `json:"environment_name,omitempty"` 172 | Hostname string `json:"hostname"` 173 | } 174 | -------------------------------------------------------------------------------- /hosted/honeybadger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue" 26 | "github.com/bobziuchkovski/cue/internal/cuetest" 27 | "reflect" 28 | "testing" 29 | ) 30 | 31 | const honeybadgerJSON = ` 32 | { 33 | "error": { 34 | "backtrace": [ 35 | { 36 | "file": "/path/github.com/bobziuchkovski/cue/frame3/file3.go", 37 | "method": "github.com/bobziuchkovski/cue/frame3.function3", 38 | "number": 3 39 | }, 40 | { 41 | "file": "/path/github.com/bobziuchkovski/cue/frame2/file2.go", 42 | "method": "github.com/bobziuchkovski/cue/frame2.function2", 43 | "number": 2 44 | }, 45 | { 46 | "file": "/path/github.com/bobziuchkovski/cue/frame1/file1.go", 47 | "method": "github.com/bobziuchkovski/cue/frame1.function1", 48 | "number": 1 49 | } 50 | ], 51 | "class": "errors.errorString", 52 | "message": "error event: error message", 53 | "tags": [ 54 | "tag1", 55 | "tag2" 56 | ] 57 | }, 58 | "notifier": { 59 | "name": "github.com/bobziuchkovski/cue", 60 | "url": "https://github.com/bobziuchkovski/cue", 61 | "version": "0.7.0" 62 | }, 63 | "request": { 64 | "component": "github.com/bobziuchkovski/cue/frame3", 65 | "context": { 66 | "extra": "extra value", 67 | "k1": "some value", 68 | "k2": 2, 69 | "k3": 3.5, 70 | "k4": true 71 | } 72 | }, 73 | "server": { 74 | "environment_name": "test", 75 | "hostname": "pegasus.bobbyz.org" 76 | } 77 | } 78 | ` 79 | 80 | const honeybadgerNoFramesJSON = ` 81 | { 82 | "error": { 83 | "class": "errors.errorString", 84 | "message": "error event: error message", 85 | "tags": [ 86 | "tag1", 87 | "tag2" 88 | ] 89 | }, 90 | "notifier": { 91 | "name": "github.com/bobziuchkovski/cue", 92 | "url": "https://github.com/bobziuchkovski/cue", 93 | "version": "0.7.0" 94 | }, 95 | "request": { 96 | "context": { 97 | "extra": "extra value", 98 | "k1": "some value", 99 | "k2": 2, 100 | "k3": 3.5, 101 | "k4": true 102 | } 103 | }, 104 | "server": { 105 | "environment_name": "test", 106 | "hostname": "pegasus.bobbyz.org" 107 | } 108 | } 109 | ` 110 | 111 | func TestHoneybadgerNilCollector(t *testing.T) { 112 | c := Honeybadger{}.New() 113 | if c != nil { 114 | t.Errorf("Expected a nil collector when the API key is missing, but got %s instead", c) 115 | } 116 | } 117 | 118 | func TestHoneybadger(t *testing.T) { 119 | checkHoneybadgerEvent(t, cuetest.ErrorEvent, honeybadgerJSON) 120 | } 121 | 122 | func TestHoneybadgerNoFrames(t *testing.T) { 123 | checkHoneybadgerEvent(t, cuetest.ErrorEventNoFrames, honeybadgerNoFramesJSON) 124 | } 125 | 126 | func TestHoneybadgerString(t *testing.T) { 127 | _ = fmt.Sprint(getHoneybadgerCollector()) 128 | } 129 | 130 | func checkHoneybadgerEvent(t *testing.T, event *cue.Event, expected string) { 131 | req, err := getHoneybadgerCollector().formatRequest(event) 132 | if err != nil { 133 | t.Errorf("Encountered unexpected error formatting http request: %s", err) 134 | } 135 | requestJSON := cuetest.ParseRequestJSON(req) 136 | expectedJSON := cuetest.ParseStringJSON(expected) 137 | 138 | version := cuetest.NestedFetch(requestJSON, "notifier", "version") 139 | if version != fmt.Sprintf("%d.%d.%d", cue.Version.Major, cue.Version.Minor, cue.Version.Patch) { 140 | t.Errorf("Invalid notifier version: %s", version) 141 | } 142 | if cuetest.NestedFetch(requestJSON, "server", "hostname") == "!(MISSING)" { 143 | t.Error("Hostname is missing from request") 144 | } 145 | 146 | cuetest.NestedDelete(requestJSON, "notifier", "version") 147 | cuetest.NestedDelete(requestJSON, "server", "hostname") 148 | cuetest.NestedDelete(expectedJSON, "notifier", "version") 149 | cuetest.NestedDelete(expectedJSON, "server", "hostname") 150 | cuetest.NestedCompare(t, requestJSON, expectedJSON) 151 | } 152 | 153 | func getHoneybadgerCollector() *honeybadgerCollector { 154 | c := Honeybadger{ 155 | Key: "test", 156 | Tags: []string{"tag1", "tag2"}, 157 | Environment: "test", 158 | ExtraContext: cue.NewContext("extra").WithValue("extra", "extra value"), 159 | }.New() 160 | hc, ok := c.(*honeybadgerCollector) 161 | if !ok { 162 | panic(fmt.Sprintf("Expected to see a *honeybadgerCollector but got %s instead", reflect.TypeOf(c))) 163 | } 164 | return hc 165 | } 166 | -------------------------------------------------------------------------------- /hosted/internal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "github.com/bobziuchkovski/cue" 25 | ) 26 | 27 | var log = cue.NewLogger("github.com/bobziuchkovski/cue/hosted") 28 | -------------------------------------------------------------------------------- /hosted/loggly.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "crypto/tls" 25 | "fmt" 26 | "github.com/bobziuchkovski/cue" 27 | "github.com/bobziuchkovski/cue/collector" 28 | "github.com/bobziuchkovski/cue/format" 29 | "io" 30 | ) 31 | 32 | const logglyNetwork = "tcp" 33 | const logglyAddress = "logs-01.loggly.com:514" 34 | const logglyEnterpriseID = 41058 35 | 36 | // Loggly represents configuration for the Loggly service. 37 | // 38 | // By default, logs are transported -in clear text-. This is very bad for 39 | // security. Please see the example for enabling TLS transport encryption with 40 | // Loggly. 41 | type Loggly struct { 42 | // Required 43 | Token string // Loggly auth token. Omit the trailing @41058 from this value. 44 | App string // Syslog app name 45 | Facility collector.Facility // Syslog facility 46 | 47 | // Optional socket config 48 | Network string // Default: "tcp" 49 | Address string // Default: "logs-01.loggly.com:514" 50 | TLS *tls.Config // TLS transport config 51 | 52 | // Optional formatting config 53 | Formatter format.Formatter // Default: format.JSONMessage 54 | Tags []string // Tags to send with every event 55 | } 56 | 57 | // New returns a new collector based on the Loggly configuration. 58 | func (l Loggly) New() cue.Collector { 59 | if l.Token == "" { 60 | log.Warn("Loggly.New called to created a collector, but the Token param is empty. Returning nil collector.") 61 | return nil 62 | } 63 | 64 | if l.App == "" { 65 | log.Warn("Loggly.New called to created a collector, but the App param is empty. Returning nil collector.") 66 | return nil 67 | } 68 | 69 | if l.Network == "" { 70 | l.Network = logglyNetwork 71 | } 72 | if l.Address == "" { 73 | l.Address = logglyAddress 74 | } 75 | if l.Formatter == nil { 76 | l.Formatter = format.JSONMessage 77 | } 78 | 79 | return &logglyCollector{ 80 | Loggly: l, 81 | syslog: collector.StructuredSyslog{ 82 | Facility: l.Facility, 83 | App: l.App, 84 | Network: l.Network, 85 | Address: l.Address, 86 | TLS: l.TLS, 87 | MessageFormatter: l.Formatter, 88 | StructuredFormatter: l.structuredFormatter(), 89 | ID: fmt.Sprintf("%s@%d", l.Token, logglyEnterpriseID), 90 | }.New(), 91 | } 92 | } 93 | 94 | func (l Loggly) structuredFormatter() format.Formatter { 95 | var literals []format.Formatter 96 | for _, tag := range l.Tags { 97 | literals = append(literals, format.Literal(fmt.Sprintf("tag=%q", tag))) 98 | } 99 | return format.Join(" ", literals...) 100 | } 101 | 102 | type logglyCollector struct { 103 | Loggly 104 | syslog cue.Collector 105 | } 106 | 107 | func (l *logglyCollector) String() string { 108 | return fmt.Sprintf("Loggly(app=%s, facility=%s, tls=%t)", l.App, l.Facility, l.TLS != nil) 109 | } 110 | 111 | func (l *logglyCollector) Collect(event *cue.Event) error { 112 | return l.syslog.Collect(event) 113 | } 114 | 115 | func (l *logglyCollector) Close() error { 116 | return l.syslog.(io.Closer).Close() 117 | } 118 | -------------------------------------------------------------------------------- /hosted/loggly_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue/collector" 26 | "github.com/bobziuchkovski/cue/internal/cuetest" 27 | "reflect" 28 | "regexp" 29 | "testing" 30 | ) 31 | 32 | func TestLogglyNilCollector(t *testing.T) { 33 | c := Loggly{App: "test"}.New() 34 | if c != nil { 35 | t.Errorf("Expected a nil collector when the token is missing, but got %s instead", c) 36 | } 37 | 38 | c = Loggly{Token: "test"}.New() 39 | if c != nil { 40 | t.Errorf("Expected a nil collector when the app is missing, but got %s instead", c) 41 | } 42 | } 43 | 44 | func TestLogglyDefaultHostNet(t *testing.T) { 45 | c := Loggly{Token: "test", App: "test"}.New() 46 | if c == nil { 47 | t.Error("Expected to get a non-nil collector with Token and App specified, but got a nil collector instead") 48 | } 49 | } 50 | 51 | func TestLoggly(t *testing.T) { 52 | recorder := cuetest.NewTCPRecorder() 53 | recorder.Start() 54 | defer recorder.Close() 55 | 56 | c := getLogglyCollector("tcp", recorder.Address()) 57 | 58 | err := c.Collect(cuetest.DebugEvent) 59 | if err != nil { 60 | t.Errorf("Encountered unexpected error: %s", err) 61 | } 62 | cuetest.CloseCollector(c) 63 | 64 | pattern := `<167>1 2006-01-02T15:04:00.000000(Z|[-+]\d{2}:\d{2}) \S+ testapp testapp\[\d+\] - \[test@41058 tag="tag1" tag="tag2"\] debug event {"k1":"some value","k2":2,"k3":3.5,"k4":true}\n` 65 | re := regexp.MustCompile(pattern) 66 | 67 | if !re.Match(recorder.Contents()) { 68 | t.Errorf("Expected content %q to match pattern %q but it didn't", recorder.Contents(), pattern) 69 | } 70 | } 71 | 72 | func TestLogglyString(t *testing.T) { 73 | _ = fmt.Sprint(getLogglyCollector("tcp", "localhost:12345")) 74 | } 75 | 76 | func getLogglyCollector(net, addr string) *logglyCollector { 77 | c := Loggly{ 78 | Token: "test", 79 | App: "testapp", 80 | Tags: []string{"tag1", "tag2"}, 81 | Facility: collector.LOCAL4, 82 | Network: net, 83 | Address: addr, 84 | }.New() 85 | lc, ok := c.(*logglyCollector) 86 | if !ok { 87 | panic(fmt.Sprintf("Expected to see a *logglyCollector but got %s instead", reflect.TypeOf(c))) 88 | } 89 | return lc 90 | } 91 | -------------------------------------------------------------------------------- /hosted/opbeat.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "bytes" 25 | "encoding/json" 26 | "fmt" 27 | "github.com/bobziuchkovski/cue" 28 | "github.com/bobziuchkovski/cue/collector" 29 | "github.com/bobziuchkovski/cue/format" 30 | "net/http" 31 | ) 32 | 33 | // Opbeat represents configuration for the Opbeat service. Collected events 34 | // are sent to Opbeat at matching event levels (debug, info, etc.), complete 35 | // with relevant stack trace. 36 | type Opbeat struct { 37 | // Required 38 | Token string // Auth token 39 | AppID string // Application ID 40 | OrganizationID string // Organization ID 41 | 42 | // Optional 43 | ExtraContext cue.Context // Additional context values to send with every event 44 | } 45 | 46 | // New returns a new collector based on the Opbeat configuration. 47 | func (o Opbeat) New() cue.Collector { 48 | if o.Token == "" || o.AppID == "" || o.OrganizationID == "" { 49 | log.Warn("Opbeat.New called to created a collector, but Token, AppID, or OrganizationID param is empty. Returning nil collector.") 50 | return nil 51 | } 52 | return &opbeatCollector{ 53 | Opbeat: o, 54 | http: collector.HTTP{RequestFormatter: o.formatRequest}.New(), 55 | } 56 | } 57 | 58 | func (o Opbeat) formatRequest(event *cue.Event) (request *http.Request, err error) { 59 | body := format.RenderBytes(o.formatBody, event) 60 | url := fmt.Sprintf("https://intake.opbeat.com/api/v1/organizations/%s/apps/%s/errors/", o.OrganizationID, o.AppID) 61 | request, err = http.NewRequest("POST", url, bytes.NewReader(body)) 62 | if err != nil { 63 | return 64 | } 65 | request.Header.Set("Accept", "application/json") 66 | request.Header.Set("Content-Type", "application/json") 67 | request.Header.Set("Authorization", "Bearer "+o.Token) 68 | return 69 | } 70 | 71 | func (o Opbeat) formatBody(buffer format.Buffer, event *cue.Event) { 72 | post := &opbeatPost{ 73 | Timestamp: event.Time.UTC().Format("2006-01-02T15:04:05.000Z"), 74 | Level: opbeatLevel(event.Level), 75 | Logger: event.Context.Name(), 76 | Message: format.RenderString(format.MessageWithError, event), 77 | Culprit: o.culpritFor(event), 78 | Extra: cue.JoinContext("", event.Context, o.ExtraContext).Fields(), 79 | Exception: o.exceptionFor(event), 80 | Stacktrace: o.stacktraceFor(event), 81 | Machine: opbeatMachine{ 82 | Hostname: format.RenderString(format.FQDN, event), 83 | }, 84 | } 85 | marshalled, _ := json.Marshal(post) 86 | buffer.Append(marshalled) 87 | } 88 | 89 | func (o Opbeat) culpritFor(event *cue.Event) string { 90 | if len(event.Frames) == 0 || event.Frames[0].Function == cue.UnknownFunction { 91 | return "" 92 | } 93 | return event.Frames[0].Function 94 | } 95 | 96 | func (o Opbeat) exceptionFor(event *cue.Event) *opbeatException { 97 | var exception *opbeatException 98 | if event.Level == cue.ERROR || event.Level == cue.FATAL { 99 | exception = &opbeatException{ 100 | Type: format.RenderString(format.ErrorType, event), 101 | Value: event.Error.Error(), 102 | } 103 | if len(event.Frames) != 0 { 104 | exception.Module = event.Frames[0].Package 105 | } 106 | } 107 | return exception 108 | } 109 | 110 | func (o Opbeat) stacktraceFor(event *cue.Event) *opbeatStacktrace { 111 | if len(event.Frames) == 0 { 112 | return nil 113 | } 114 | 115 | stacktrace := &opbeatStacktrace{} 116 | for i := len(event.Frames) - 1; i >= 0; i-- { 117 | stacktrace.Frames = append(stacktrace.Frames, &opbeatFrame{ 118 | Filename: event.Frames[i].File, 119 | Function: event.Frames[i].Function, 120 | Lineno: event.Frames[i].Line, 121 | }) 122 | } 123 | return stacktrace 124 | } 125 | 126 | type opbeatCollector struct { 127 | Opbeat 128 | http cue.Collector 129 | } 130 | 131 | func (o *opbeatCollector) String() string { 132 | return fmt.Sprintf("Opbeat(appId=%s)", o.AppID) 133 | } 134 | 135 | func (o *opbeatCollector) Collect(event *cue.Event) error { 136 | return o.http.Collect(event) 137 | } 138 | 139 | type opbeatPost struct { 140 | Timestamp string `json:"timestamp"` 141 | Level string `json:"level"` 142 | Logger string `json:"logger,omitempty"` 143 | Message string `json:"message"` 144 | Culprit string `json:"culprit,omitempty"` 145 | Machine opbeatMachine `json:"machine"` 146 | Extra cue.Fields `json:"extra"` 147 | Exception *opbeatException `json:"exception,omitempty"` 148 | Stacktrace *opbeatStacktrace `json:"stacktrace,omitempty"` 149 | } 150 | 151 | type opbeatException struct { 152 | Type string `json:"type"` 153 | Value string `json:"value"` 154 | Module string `json:"module,omitempty"` 155 | } 156 | 157 | type opbeatStacktrace struct { 158 | Frames []*opbeatFrame `json:"frames"` 159 | } 160 | 161 | type opbeatFrame struct { 162 | Filename string `json:"filename"` 163 | Function string `json:"function"` 164 | Lineno int `json:"lineno"` 165 | } 166 | 167 | type opbeatMachine struct { 168 | Hostname string `json:"hostname"` 169 | } 170 | 171 | func opbeatLevel(level cue.Level) string { 172 | switch level { 173 | case cue.DEBUG: 174 | return "debug" 175 | case cue.INFO: 176 | return "info" 177 | case cue.WARN: 178 | return "warning" 179 | case cue.ERROR: 180 | return "error" 181 | case cue.FATAL: 182 | return "fatal" 183 | default: 184 | panic("cue/hosted: BUG invalid level") 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /hosted/opbeat_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue" 26 | "github.com/bobziuchkovski/cue/internal/cuetest" 27 | "reflect" 28 | "testing" 29 | ) 30 | 31 | const opbeatJSON = ` 32 | { 33 | "culprit": "github.com/bobziuchkovski/cue/frame3.function3", 34 | "exception": { 35 | "module": "github.com/bobziuchkovski/cue/frame3", 36 | "type": "errors.errorString", 37 | "value": "error message" 38 | }, 39 | "extra": { 40 | "extra": "extra value", 41 | "k1": "some value", 42 | "k2": 2, 43 | "k3": 3.5, 44 | "k4": true 45 | }, 46 | "level": "error", 47 | "logger": "test context", 48 | "machine": { 49 | "hostname": "pegasus.bobbyz.org" 50 | }, 51 | "message": "error event: error message", 52 | "stacktrace": { 53 | "frames": [ 54 | { 55 | "filename": "/path/github.com/bobziuchkovski/cue/frame1/file1.go", 56 | "function": "github.com/bobziuchkovski/cue/frame1.function1", 57 | "lineno": 1 58 | }, 59 | { 60 | "filename": "/path/github.com/bobziuchkovski/cue/frame2/file2.go", 61 | "function": "github.com/bobziuchkovski/cue/frame2.function2", 62 | "lineno": 2 63 | }, 64 | { 65 | "filename": "/path/github.com/bobziuchkovski/cue/frame3/file3.go", 66 | "function": "github.com/bobziuchkovski/cue/frame3.function3", 67 | "lineno": 3 68 | } 69 | ] 70 | }, 71 | "timestamp": "2006-01-02T22:04:00.000Z" 72 | } 73 | ` 74 | 75 | const opbeatNoFramesJSON = ` 76 | { 77 | "exception": { 78 | "type": "errors.errorString", 79 | "value": "error message" 80 | }, 81 | "extra": { 82 | "extra": "extra value", 83 | "k1": "some value", 84 | "k2": 2, 85 | "k3": 3.5, 86 | "k4": true 87 | }, 88 | "level": "error", 89 | "logger": "test context", 90 | "machine": { 91 | "hostname": "pegasus.bobbyz.org" 92 | }, 93 | "message": "error event: error message", 94 | "timestamp": "2006-01-02T22:04:00.000Z" 95 | } 96 | ` 97 | 98 | func TestOpbeatNilCollector(t *testing.T) { 99 | c := Opbeat{}.New() 100 | if c != nil { 101 | t.Errorf("Expected a nil collector when the token is missing, but got %s instead", c) 102 | } 103 | } 104 | 105 | func TestOpbeat(t *testing.T) { 106 | checkOpbeatEvent(t, cuetest.ErrorEvent, opbeatJSON) 107 | } 108 | 109 | func TestOpbeatNoFrames(t *testing.T) { 110 | checkOpbeatEvent(t, cuetest.ErrorEventNoFrames, opbeatNoFramesJSON) 111 | } 112 | 113 | func TestOpbeatString(t *testing.T) { 114 | _ = fmt.Sprint(getOpbeatCollector()) 115 | } 116 | 117 | func TestOpbeatLevels(t *testing.T) { 118 | m := map[cue.Level]string{ 119 | cue.DEBUG: "debug", 120 | cue.INFO: "info", 121 | cue.WARN: "warning", 122 | cue.ERROR: "error", 123 | cue.FATAL: "fatal", 124 | } 125 | for k, v := range m { 126 | if opbeatLevel(k) != v { 127 | t.Errorf("Expected cue level %q to map to opbeat level %q but it didn't", k, v) 128 | } 129 | } 130 | } 131 | 132 | func checkOpbeatEvent(t *testing.T, event *cue.Event, expected string) { 133 | req, err := getOpbeatCollector().formatRequest(event) 134 | if err != nil { 135 | t.Errorf("Encountered unexpected error formatting http request: %s", err) 136 | } 137 | requestJSON := cuetest.ParseRequestJSON(req) 138 | expectedJSON := cuetest.ParseStringJSON(expected) 139 | 140 | if cuetest.NestedFetch(requestJSON, "machine", "hostname") == "!(MISSING)" { 141 | t.Error("Hostname is missing from request") 142 | } 143 | if cuetest.NestedFetch(requestJSON, "timestamp") == "!(MISSING)" { 144 | t.Error("Timestamp is missing from request") 145 | } 146 | 147 | cuetest.NestedDelete(requestJSON, "machine", "hostname") 148 | cuetest.NestedDelete(expectedJSON, "machine", "hostname") 149 | cuetest.NestedDelete(requestJSON, "timestamp") 150 | cuetest.NestedDelete(expectedJSON, "timestamp") 151 | cuetest.NestedCompare(t, requestJSON, expectedJSON) 152 | } 153 | 154 | func getOpbeatCollector() *opbeatCollector { 155 | c := Opbeat{ 156 | Token: "test", 157 | AppID: "app", 158 | OrganizationID: "org", 159 | ExtraContext: cue.NewContext("extra").WithValue("extra", "extra value"), 160 | }.New() 161 | oc, ok := c.(*opbeatCollector) 162 | if !ok { 163 | panic(fmt.Sprintf("Expected to see a *opbeatCollector but got %s instead", reflect.TypeOf(c))) 164 | } 165 | return oc 166 | } 167 | -------------------------------------------------------------------------------- /hosted/rollbar.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "bytes" 25 | "encoding/json" 26 | "fmt" 27 | "github.com/bobziuchkovski/cue" 28 | "github.com/bobziuchkovski/cue/collector" 29 | "github.com/bobziuchkovski/cue/format" 30 | "net/http" 31 | "runtime" 32 | ) 33 | 34 | // Rollbar represents configuration for the Rollbar service. Collected events 35 | // are sent to Rollbar at matching event levels (debug, info, etc.), complete 36 | // with relevant stack trace. 37 | type Rollbar struct { 38 | // Required 39 | Token string // Auth token 40 | Environment string // Environment name ("development", "production", etc.) 41 | 42 | // Optional 43 | ExtraContext cue.Context // Additional context values to send with every event 44 | ProjectVersion string // Project version (SHA value, semantic version, etc.) 45 | ProjectFramework string // Project framework name 46 | } 47 | 48 | // New returns a new collector based on the Rollbar configuration. 49 | func (r Rollbar) New() cue.Collector { 50 | if r.Token == "" || r.Environment == "" { 51 | log.Warn("Rollbar.New called to created a collector, but Token or Environment param is empty. Returning nil collector.") 52 | return nil 53 | } 54 | return &rollbarCollector{ 55 | Rollbar: r, 56 | http: collector.HTTP{RequestFormatter: r.formatRequest}.New(), 57 | } 58 | } 59 | 60 | func (r Rollbar) formatRequest(event *cue.Event) (request *http.Request, err error) { 61 | body := format.RenderBytes(r.formatBody, event) 62 | request, err = http.NewRequest("POST", "https://api.rollbar.com/api/1/item/", bytes.NewReader(body)) 63 | if err != nil { 64 | return 65 | } 66 | request.Header.Set("Accept", "application/json") 67 | request.Header.Set("Content-Type", "application/json") 68 | return 69 | } 70 | 71 | func (r Rollbar) formatBody(buffer format.Buffer, event *cue.Event) { 72 | codever := r.ProjectVersion 73 | if len(codever) > 40 { 74 | codever = codever[:40] 75 | } 76 | 77 | bodyFormatter := r.formatTrace 78 | if event.Level > cue.ERROR || len(event.Frames) == 0 { 79 | bodyFormatter = r.formatMessage 80 | } 81 | 82 | contextJSON, _ := json.Marshal(cue.JoinContext("", event.Context, r.ExtraContext).Fields()) 83 | marshalled, _ := json.Marshal(&rollbarPost{ 84 | Token: r.Token, 85 | Data: rollbarData{ 86 | Timestamp: event.Time.Unix(), 87 | Environment: r.Environment, 88 | Framework: r.ProjectFramework, 89 | Level: rollbarLevel(event.Level), 90 | Body: bodyFormatter(event), 91 | Custom: json.RawMessage(contextJSON), 92 | CodeVersion: codever, 93 | Platform: runtime.GOOS, 94 | Server: rollbarServer{ 95 | Host: format.RenderString(format.FQDN, event), 96 | }, 97 | Notifier: rollbarNotifier{ 98 | Name: "github.com/bobziuchkovski/cue", 99 | Version: fmt.Sprintf("%d.%d.%d", cue.Version.Major, cue.Version.Minor, cue.Version.Patch), 100 | }, 101 | Language: "go", 102 | }, 103 | }) 104 | buffer.Append(marshalled) 105 | } 106 | 107 | func (r Rollbar) formatMessage(event *cue.Event) json.RawMessage { 108 | marshalled, _ := json.Marshal(&rollbarMessage{ 109 | Message: &rollbarMessageBody{ 110 | Body: format.RenderString(format.MessageWithError, event), 111 | }, 112 | }) 113 | return json.RawMessage(marshalled) 114 | } 115 | 116 | func (r Rollbar) formatTrace(event *cue.Event) json.RawMessage { 117 | body := &rollbarTraceBody{ 118 | Trace: rollbarTrace{ 119 | Exception: rollbarException{ 120 | Class: format.RenderString(format.ErrorType, event), 121 | Message: event.Error.Error(), 122 | Description: format.RenderString(format.MessageWithError, event), 123 | }, 124 | }, 125 | } 126 | for i := len(event.Frames) - 1; i >= 0; i-- { 127 | body.Trace.Frames = append(body.Trace.Frames, &rollbarFrame{ 128 | Filename: event.Frames[i].File, 129 | Lineno: event.Frames[i].Line, 130 | Method: event.Frames[i].Function, 131 | }) 132 | } 133 | 134 | marshalled, _ := json.Marshal(body) 135 | return json.RawMessage(marshalled) 136 | } 137 | 138 | type rollbarCollector struct { 139 | Rollbar 140 | http cue.Collector 141 | } 142 | 143 | func (r *rollbarCollector) String() string { 144 | return fmt.Sprintf("Rollbar(environment=%s)", r.Environment) 145 | } 146 | 147 | func (r *rollbarCollector) Collect(event *cue.Event) error { 148 | return r.http.Collect(event) 149 | } 150 | 151 | type rollbarPost struct { 152 | Token string `json:"access_token"` 153 | Data rollbarData `json:"data"` 154 | } 155 | 156 | type rollbarData struct { 157 | Environment string `json:"environment"` 158 | Body json.RawMessage `json:"body"` 159 | Level string `json:"level"` 160 | Timestamp int64 `json:"timestamp"` 161 | CodeVersion string `json:"code_version,omitempty"` 162 | Platform string `json:"platform"` 163 | Language string `json:"language"` 164 | Framework string `json:"framework,omitempty"` 165 | Server rollbarServer `json:"server"` 166 | Custom json.RawMessage `json:"custom"` 167 | Notifier rollbarNotifier `json:"notifier"` 168 | } 169 | 170 | type rollbarServer struct { 171 | Host string `json:"host"` 172 | } 173 | 174 | type rollbarNotifier struct { 175 | Name string `json:"name"` 176 | Version string `json:"version"` 177 | } 178 | 179 | type rollbarMessage struct { 180 | Message *rollbarMessageBody `json:"message"` 181 | } 182 | 183 | type rollbarMessageBody struct { 184 | Body string `json:"body"` 185 | } 186 | 187 | type rollbarTraceBody struct { 188 | Trace rollbarTrace `json:"trace"` 189 | } 190 | 191 | type rollbarTrace struct { 192 | Frames []*rollbarFrame `json:"frames"` 193 | Exception rollbarException `json:"exception"` 194 | } 195 | 196 | type rollbarFrame struct { 197 | Filename string `json:"filename"` 198 | Lineno int `json:"lineno"` 199 | Method string `json:"method"` 200 | } 201 | 202 | type rollbarException struct { 203 | Class string `json:"class"` 204 | Message string `json:"message"` 205 | Description string `json:"description"` 206 | } 207 | 208 | func rollbarLevel(level cue.Level) string { 209 | switch level { 210 | case cue.DEBUG: 211 | return "debug" 212 | case cue.INFO: 213 | return "info" 214 | case cue.WARN: 215 | return "warning" 216 | case cue.ERROR: 217 | return "error" 218 | case cue.FATAL: 219 | return "critical" 220 | default: 221 | panic("cue/hosted: BUG invalid cue level") 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /hosted/rollbar_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue" 26 | "github.com/bobziuchkovski/cue/internal/cuetest" 27 | "reflect" 28 | "testing" 29 | ) 30 | 31 | const rollbarJSON = ` 32 | { 33 | "access_token": "test", 34 | "data": { 35 | "body": { 36 | "trace": { 37 | "exception": { 38 | "class": "errors.errorString", 39 | "description": "error event: error message", 40 | "message": "error message" 41 | }, 42 | "frames": [ 43 | { 44 | "filename": "/path/github.com/bobziuchkovski/cue/frame1/file1.go", 45 | "lineno": 1, 46 | "method": "github.com/bobziuchkovski/cue/frame1.function1" 47 | }, 48 | { 49 | "filename": "/path/github.com/bobziuchkovski/cue/frame2/file2.go", 50 | "lineno": 2, 51 | "method": "github.com/bobziuchkovski/cue/frame2.function2" 52 | }, 53 | { 54 | "filename": "/path/github.com/bobziuchkovski/cue/frame3/file3.go", 55 | "lineno": 3, 56 | "method": "github.com/bobziuchkovski/cue/frame3.function3" 57 | } 58 | ] 59 | } 60 | }, 61 | "code_version": "1.2.3", 62 | "custom": { 63 | "extra": "extra value", 64 | "k1": "some value", 65 | "k2": 2, 66 | "k3": 3.5, 67 | "k4": true 68 | }, 69 | "environment": "test", 70 | "framework": "sliced-bread", 71 | "language": "go", 72 | "level": "error", 73 | "notifier": { 74 | "name": "github.com/bobziuchkovski/cue", 75 | "version": "0.7.0" 76 | }, 77 | "platform": "darwin", 78 | "server": { 79 | "host": "pegasus.bobbyz.org" 80 | }, 81 | "timestamp": 1136239440 82 | } 83 | } 84 | ` 85 | 86 | const rollbarNoFramesJSON = ` 87 | { 88 | "access_token": "test", 89 | "data": { 90 | "body": { 91 | "message": { 92 | "body": "error event: error message" 93 | } 94 | }, 95 | "code_version": "1.2.3", 96 | "custom": { 97 | "extra": "extra value", 98 | "k1": "some value", 99 | "k2": 2, 100 | "k3": 3.5, 101 | "k4": true 102 | }, 103 | "environment": "test", 104 | "framework": "sliced-bread", 105 | "language": "go", 106 | "level": "error", 107 | "notifier": { 108 | "name": "github.com/bobziuchkovski/cue", 109 | "version": "0.7.0" 110 | }, 111 | "platform": "darwin", 112 | "server": { 113 | "host": "pegasus.bobbyz.org" 114 | }, 115 | "timestamp": 1136239440 116 | } 117 | } 118 | ` 119 | 120 | func TestRollbarNilCollector(t *testing.T) { 121 | c := Rollbar{}.New() 122 | if c != nil { 123 | t.Errorf("Expected a nil collector when the token is missing, but got %s instead", c) 124 | } 125 | } 126 | 127 | func TestRollbar(t *testing.T) { 128 | checkRollbarEvent(t, cuetest.ErrorEvent, rollbarJSON) 129 | } 130 | 131 | func TestRollbarNoFrames(t *testing.T) { 132 | checkRollbarEvent(t, cuetest.ErrorEventNoFrames, rollbarNoFramesJSON) 133 | } 134 | 135 | func TestRollbarString(t *testing.T) { 136 | _ = fmt.Sprint(getRollbarCollector()) 137 | } 138 | 139 | func TestRollbarLevels(t *testing.T) { 140 | m := map[cue.Level]string{ 141 | cue.DEBUG: "debug", 142 | cue.INFO: "info", 143 | cue.WARN: "warning", 144 | cue.ERROR: "error", 145 | cue.FATAL: "critical", 146 | } 147 | for k, v := range m { 148 | if rollbarLevel(k) != v { 149 | t.Errorf("Expected cue level %q to map to rollbar level %q but it didn't", k, v) 150 | } 151 | } 152 | } 153 | 154 | func checkRollbarEvent(t *testing.T, event *cue.Event, expected string) { 155 | req, err := getRollbarCollector().formatRequest(event) 156 | if err != nil { 157 | t.Errorf("Encountered unexpected error formatting http request: %s", err) 158 | } 159 | requestJSON := cuetest.ParseRequestJSON(req) 160 | expectedJSON := cuetest.ParseStringJSON(expected) 161 | 162 | version := cuetest.NestedFetch(requestJSON, "data", "notifier", "version") 163 | if version != fmt.Sprintf("%d.%d.%d", cue.Version.Major, cue.Version.Minor, cue.Version.Patch) { 164 | t.Errorf("Invalid notifier version: %s", version) 165 | } 166 | if cuetest.NestedFetch(requestJSON, "data", "server", "host") == "!(MISSING)" { 167 | t.Error("Server host is missing from request") 168 | } 169 | if cuetest.NestedFetch(requestJSON, "data", "platform") == "!(MISSING)" { 170 | t.Error("Platform is missing from request") 171 | } 172 | if cuetest.NestedFetch(requestJSON, "data", "timestamp") == "!(MISSING)" { 173 | t.Error("Timestamp is missing from request") 174 | } 175 | 176 | cuetest.NestedDelete(requestJSON, "data", "notifier", "version") 177 | cuetest.NestedDelete(expectedJSON, "data", "notifier", "version") 178 | cuetest.NestedDelete(requestJSON, "data", "server", "host") 179 | cuetest.NestedDelete(expectedJSON, "data", "server", "host") 180 | cuetest.NestedDelete(requestJSON, "data", "platform") 181 | cuetest.NestedDelete(expectedJSON, "data", "platform") 182 | cuetest.NestedDelete(requestJSON, "data", "timestamp") 183 | cuetest.NestedDelete(expectedJSON, "data", "timestamp") 184 | cuetest.NestedCompare(t, requestJSON, expectedJSON) 185 | } 186 | 187 | func getRollbarCollector() *rollbarCollector { 188 | c := Rollbar{ 189 | Token: "test", 190 | Environment: "test", 191 | ProjectVersion: "1.2.3", 192 | ProjectFramework: "sliced-bread", 193 | ExtraContext: cue.NewContext("extra").WithValue("extra", "extra value"), 194 | }.New() 195 | rc, ok := c.(*rollbarCollector) 196 | if !ok { 197 | panic(fmt.Sprintf("Expected to see a *rollbarCollector but got %s instead", reflect.TypeOf(c))) 198 | } 199 | return rc 200 | } 201 | -------------------------------------------------------------------------------- /hosted/sentry_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "fmt" 25 | "github.com/bobziuchkovski/cue" 26 | "github.com/bobziuchkovski/cue/internal/cuetest" 27 | "reflect" 28 | "testing" 29 | ) 30 | 31 | const sentryJSON = ` 32 | { 33 | "culprit": "github.com/bobziuchkovski/cue/frame3.function3", 34 | "event_id": "c51c8551adaa42ee9b9fd0b63d462e8d", 35 | "exception": { 36 | "module": "github.com/bobziuchkovski/cue/frame3", 37 | "stacktrace": { 38 | "frames": [ 39 | { 40 | "filename": "/path/github.com/bobziuchkovski/cue/frame1/file1.go", 41 | "function": "github.com/bobziuchkovski/cue/frame1.function1", 42 | "lineno": 1, 43 | "module": "github.com/bobziuchkovski/cue/frame1" 44 | }, 45 | { 46 | "filename": "/path/github.com/bobziuchkovski/cue/frame2/file2.go", 47 | "function": "github.com/bobziuchkovski/cue/frame2.function2", 48 | "lineno": 2, 49 | "module": "github.com/bobziuchkovski/cue/frame2" 50 | }, 51 | { 52 | "filename": "/path/github.com/bobziuchkovski/cue/frame3/file3.go", 53 | "function": "github.com/bobziuchkovski/cue/frame3.function3", 54 | "lineno": 3, 55 | "module": "github.com/bobziuchkovski/cue/frame3" 56 | } 57 | ] 58 | }, 59 | "type": "errors.errorString", 60 | "value": "error event" 61 | }, 62 | "level": "error", 63 | "logger": "test context", 64 | "message": "error event: error message", 65 | "platform": "go", 66 | "server_name": "pegasus.bobbyz.org", 67 | "tags": [ 68 | [ 69 | "extra", 70 | "extra value" 71 | ], 72 | [ 73 | "k1", 74 | "some value" 75 | ], 76 | [ 77 | "k2", 78 | "2" 79 | ], 80 | [ 81 | "k3", 82 | "3.5" 83 | ], 84 | [ 85 | "k4", 86 | "true" 87 | ] 88 | ], 89 | "timestamp": "2006-01-02T22:04:00" 90 | } 91 | ` 92 | 93 | const sentryNoFramesJSON = ` 94 | { 95 | "event_id": "679989e954034f948cc5e5cb220d32aa", 96 | "exception": { 97 | "type": "errors.errorString", 98 | "value": "error event" 99 | }, 100 | "level": "error", 101 | "logger": "test context", 102 | "message": "error event: error message", 103 | "platform": "go", 104 | "server_name": "pegasus.bobbyz.org", 105 | "tags": [ 106 | [ 107 | "extra", 108 | "extra value" 109 | ], 110 | [ 111 | "k1", 112 | "some value" 113 | ], 114 | [ 115 | "k2", 116 | "2" 117 | ], 118 | [ 119 | "k3", 120 | "3.5" 121 | ], 122 | [ 123 | "k4", 124 | "true" 125 | ] 126 | ], 127 | "timestamp": "2006-01-02T22:04:00" 128 | } 129 | ` 130 | 131 | func TestSentryNilCollector(t *testing.T) { 132 | c := Sentry{}.New() 133 | if c != nil { 134 | t.Errorf("Expected a nil collector when the DSN is missing, but got %s instead", c) 135 | } 136 | } 137 | 138 | func TestSentry(t *testing.T) { 139 | checkSentryEvent(t, cuetest.ErrorEvent, sentryJSON) 140 | } 141 | 142 | func TestSentryNoFrames(t *testing.T) { 143 | checkSentryEvent(t, cuetest.ErrorEventNoFrames, sentryNoFramesJSON) 144 | } 145 | 146 | func TestSentryString(t *testing.T) { 147 | _ = fmt.Sprint(getSentryCollector()) 148 | } 149 | 150 | func TestSentryValidDSN(t *testing.T) { 151 | if validDSN(":bogus") { 152 | t.Errorf("%q should register as an invalid DSN, but that's not the case", ":bogus") 153 | } 154 | if validDSN("http://sentry.private") { 155 | t.Errorf("%q should register as an invalid DSN due to missing user, but that's not the case", "http://bob@sentry.private") 156 | } 157 | if validDSN("http://:pass@sentry.private") { 158 | t.Errorf("%q should register as an invalid DSN due to missing user, but that's not the case", "http://bob@sentry.private") 159 | } 160 | if validDSN("http://bob@sentry.private") { 161 | t.Errorf("%q should register as an invalid DSN due to missing password, but that's not the case", "http://bob@sentry.private") 162 | } 163 | } 164 | 165 | func TestSentryLevels(t *testing.T) { 166 | m := map[cue.Level]string{ 167 | cue.DEBUG: "debug", 168 | cue.INFO: "info", 169 | cue.WARN: "warning", 170 | cue.ERROR: "error", 171 | cue.FATAL: "fatal", 172 | } 173 | for k, v := range m { 174 | if sentryLevel(k) != v { 175 | t.Errorf("Expected cue level %q to map to sentry level %q but it didn't", k, v) 176 | } 177 | } 178 | } 179 | 180 | func checkSentryEvent(t *testing.T, event *cue.Event, expected string) { 181 | req, err := getSentryCollector().formatRequest(event) 182 | if err != nil { 183 | t.Errorf("Encountered unexpected error formatting http request: %s", err) 184 | } 185 | requestJSON := cuetest.ParseRequestJSON(req) 186 | expectedJSON := cuetest.ParseStringJSON(expected) 187 | 188 | if cuetest.NestedFetch(requestJSON, "event_id") == "!(MISSING)" { 189 | t.Error("event_id is missing from request") 190 | } 191 | if cuetest.NestedFetch(requestJSON, "server_name") == "!(MISSING)" { 192 | t.Error("server_name is missing from request") 193 | } 194 | if cuetest.NestedFetch(requestJSON, "timestamp") == "!(MISSING)" { 195 | t.Error("timestamp is missing from request") 196 | } 197 | 198 | cuetest.NestedDelete(requestJSON, "event_id") 199 | cuetest.NestedDelete(expectedJSON, "event_id") 200 | cuetest.NestedDelete(requestJSON, "server_name") 201 | cuetest.NestedDelete(expectedJSON, "server_name") 202 | cuetest.NestedDelete(requestJSON, "timestamp") 203 | cuetest.NestedDelete(expectedJSON, "timestamp") 204 | cuetest.NestedCompare(t, requestJSON, expectedJSON) 205 | } 206 | 207 | func getSentryCollector() *sentryCollector { 208 | c := Sentry{ 209 | DSN: "https://public:private@app.getsentry.com.bogus/12345", 210 | ExtraContext: cue.NewContext("extra").WithValue("extra", "extra value"), 211 | }.New() 212 | sc, ok := c.(*sentryCollector) 213 | if !ok { 214 | panic(fmt.Sprintf("Expected to see a *sentryCollector but got %s instead", reflect.TypeOf(c))) 215 | } 216 | return sc 217 | } 218 | -------------------------------------------------------------------------------- /hosted/sf_bundle.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFBzCCA++gAwIBAgICAgEwDQYJKoZIhvcNAQEFBQAwaDELMAkGA1UEBhMCVVMx 3 | JTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsT 4 | KVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2 5 | MTExNjAxMTU0MFoXDTI2MTExNjAxMTU0MFowgdwxCzAJBgNVBAYTAlVTMRAwDgYD 6 | VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy 7 | ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTkwNwYDVQQLEzBodHRwOi8vY2VydGlm 8 | aWNhdGVzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkxMTAvBgNVBAMTKFN0 9 | YXJmaWVsZCBTZWN1cmUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxETAPBgNVBAUT 10 | CDEwNjg4NDM1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4qddo+1m 11 | 72ovKzYf3Y3TBQKgyg9eGa44cs8W2lRKy0gK9KFzEWWFQ8lbFwyaK74PmFF6YCkN 12 | bN7i6OUVTVb/kNGnpgQ/YAdKym+lEOez+FyxvCsq3AF59R019Xoog/KTc4KJrGBt 13 | y8JIwh3UBkQXPKwBR6s+cIQJC7ggCEAgh6FjGso+g9I3s5iNMj83v6G3W1/eXDOS 14 | zz4HzrlIS+LwVVAv+HBCidGTlopj2WYN5lhuuW2QvcrchGbyOY5bplhVc8tibBvX 15 | IBY7LFn1y8hWMkpQJ7pV06gBy3KpdIsMrTrlFbYq32X43or174Q7+edUZQuAvUdF 16 | pfBE2FM7voDxLwIDAQABo4IBRDCCAUAwHQYDVR0OBBYEFElLUifRG7zyoSFqYntR 17 | QnqK19VWMB8GA1UdIwQYMBaAFL9ft9HO3R+G9FtVrNzXEMIOqYjnMBIGA1UdEwEB 18 | /wQIMAYBAf8CAQAwOQYIKwYBBQUHAQEELTArMCkGCCsGAQUFBzABhh1odHRwOi8v 19 | b2NzcC5zdGFyZmllbGR0ZWNoLmNvbTBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8v 20 | Y2VydGlmaWNhdGVzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkvc2Zyb290 21 | LmNybDBRBgNVHSAESjBIMEYGBFUdIAAwPjA8BggrBgEFBQcCARYwaHR0cDovL2Nl 22 | cnRpZmljYXRlcy5zdGFyZmllbGR0ZWNoLmNvbS9yZXBvc2l0b3J5MA4GA1UdDwEB 23 | /wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAhlK6sx+mXmuQpmQq/EWyrp8+s2Kv 24 | 2x9nxL3KoS/HnA0hV9D4NiHOOiU+eHaz2d283vtshF8Mow0S6xE7cV+AHvEfbQ5f 25 | wezUpfdlux9MlQETsmqcC+sfnbHn7RkNvIV88xe9WWOupxoFzUfjLZZiUTIKCGhL 26 | Indf90XcYd70yysiKUQl0p8Ld3qhJnxK1w/C0Ty6DqeVmlsFChD5VV/Bl4t0zF4o 27 | aRN+0AqNnQ9gVHrEjBs1D3R6cLKCzx214orbKsayUWm/EheSYBeqPVsJ+IdlHaek 28 | KOUiAgOCRJo0Y577KM/ozS4OUiDtSss4fJ2ubnnXlSyokfOGASGRS7VApA== 29 | -----END CERTIFICATE----- 30 | -----BEGIN CERTIFICATE----- 31 | MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl 32 | MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp 33 | U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw 34 | NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE 35 | ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp 36 | ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 37 | DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf 38 | 8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN 39 | +lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 40 | X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa 41 | K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA 42 | 1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G 43 | A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR 44 | zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 45 | YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD 46 | bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w 47 | DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 48 | L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D 49 | eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl 50 | xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp 51 | VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY 52 | WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= 53 | -----END CERTIFICATE----- 54 | -------------------------------------------------------------------------------- /hosted/uuid.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "crypto/rand" 25 | ) 26 | 27 | // The uuid function is used by the sentry collector to generate unique event 28 | // IDs. 29 | func uuid() []byte { 30 | uuid := make([]byte, 16) 31 | _, err := rand.Read(uuid) 32 | if err != nil { 33 | panic("cue/hosted: uuid() failed to read random bytes") 34 | } 35 | 36 | // The following bit twiddling is outlined in RFC 4122. In short, it 37 | // identifies the UUID as a v4 random UUID. 38 | uuid[6] = (4 << 4) | (0xf & uuid[6]) 39 | uuid[8] = (8 << 4) | (0x3f & uuid[8]) 40 | return uuid 41 | } 42 | -------------------------------------------------------------------------------- /hosted/uuid_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hosted 22 | 23 | import ( 24 | "encoding/hex" 25 | "testing" 26 | ) 27 | 28 | func TestUUID(t *testing.T) { 29 | value := hex.EncodeToString(uuid()) 30 | if value[12] != byte('4') { 31 | t.Errorf("Invalid UUID. Expected the 13th character to be '4' but got %q instead. UUID: %s", rune(value[13]), value) 32 | } 33 | switch value[16] { 34 | case byte('8'), byte('9'), byte('a'), byte('b'): 35 | // Valid 36 | default: 37 | t.Errorf("Invalid UUID. Expected the 17th character to be '8', '9', 'a', or 'b', but got %q instead. UUID: %s", rune(value[16]), value) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/cuetest/collectors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cuetest 22 | 23 | import ( 24 | "github.com/bobziuchkovski/cue" 25 | "sync" 26 | "time" 27 | ) 28 | 29 | // CapturingCollector captures events that are sent to its Collect method. 30 | type CapturingCollector struct { 31 | captured []*cue.Event 32 | cond *sync.Cond 33 | mu sync.Mutex 34 | } 35 | 36 | // NewCapturingCollector returns a new CapturingCollector instance. 37 | func NewCapturingCollector() *CapturingCollector { 38 | c := &CapturingCollector{} 39 | c.cond = sync.NewCond(&c.mu) 40 | return c 41 | } 42 | 43 | // Collect captures the input event for later inspection. 44 | func (c *CapturingCollector) Collect(event *cue.Event) error { 45 | c.mu.Lock() 46 | defer c.mu.Unlock() 47 | c.captured = append(c.captured, event) 48 | c.cond.Broadcast() 49 | return nil 50 | } 51 | 52 | // Captured returns a slice of captured events. 53 | func (c *CapturingCollector) Captured() []*cue.Event { 54 | c.mu.Lock() 55 | defer c.mu.Unlock() 56 | dup := make([]*cue.Event, len(c.captured)) 57 | for i, event := range c.captured { 58 | dup[i] = event 59 | } 60 | return dup 61 | } 62 | 63 | // WaitCaptured waits for count events to be captured. If count events aren't 64 | // captured within maxWait time, it panics. 65 | func (c *CapturingCollector) WaitCaptured(count int, maxWait time.Duration) { 66 | finished := make(chan struct{}) 67 | go c.waitAsync(count, finished) 68 | 69 | select { 70 | case <-finished: 71 | return 72 | case <-time.After(maxWait): 73 | panic("WaitCaptured timed-out waiting for events") 74 | } 75 | } 76 | 77 | func (c *CapturingCollector) waitAsync(count int, finished chan struct{}) { 78 | c.mu.Lock() 79 | defer c.mu.Unlock() 80 | for len(c.captured) != count { 81 | c.cond.Wait() 82 | } 83 | close(finished) 84 | } 85 | 86 | // String returns a string representation of the CapturingCollector. 87 | func (c *CapturingCollector) String() string { 88 | return "CapturingCollector()" 89 | } 90 | -------------------------------------------------------------------------------- /internal/cuetest/events.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cuetest 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "github.com/bobziuchkovski/cue" 27 | "time" 28 | ) 29 | 30 | // We ensure deterministic iteration order by using multiple WithValue 31 | // calls as opposed to a WithFields call. 32 | var ctx = cue.NewContext("test context"). 33 | WithValue("k1", "some value"). 34 | WithValue("k2", 2). 35 | WithValue("k3", 3.5). 36 | WithValue("k4", true) 37 | 38 | // Test events at all cue event levels. The *Event instances have 3 frames 39 | // in there Frames field while the *EventNoFrames instances have 0. 40 | var ( 41 | DebugEvent = GenerateEvent(cue.DEBUG, ctx, "debug event", nil, 3) 42 | DebugEventNoFrames = GenerateEvent(cue.DEBUG, ctx, "debug event", nil, 0) 43 | InfoEvent = GenerateEvent(cue.INFO, ctx, "info event", nil, 3) 44 | InfoEventNoFrames = GenerateEvent(cue.INFO, ctx, "info event", nil, 0) 45 | WarnEvent = GenerateEvent(cue.WARN, ctx, "warn event", nil, 3) 46 | WarnEventNoFrames = GenerateEvent(cue.WARN, ctx, "warn event", nil, 0) 47 | ErrorEvent = GenerateEvent(cue.ERROR, ctx, "error event", errors.New("error message"), 3) 48 | ErrorEventNoFrames = GenerateEvent(cue.ERROR, ctx, "error event", errors.New("error message"), 0) 49 | FatalEvent = GenerateEvent(cue.FATAL, ctx, "fatal event", errors.New("fatal message"), 3) 50 | FatalEventNoFrames = GenerateEvent(cue.FATAL, ctx, "fatal event", errors.New("fatal message"), 0) 51 | ) 52 | 53 | // GenerateEvent returns a new event for the given parameters. The frames 54 | // parameter determines how many frames to attach to the Frames field. The 55 | // generated frames follow a naming pattern based on their index. The time 56 | // used for the generated event is the same time used by the time package to 57 | // represent time formats. 58 | func GenerateEvent(level cue.Level, context cue.Context, message string, err error, frames int) *cue.Event { 59 | event := &cue.Event{ 60 | Time: testTime(), 61 | Level: level, 62 | Context: context, 63 | Message: message, 64 | Error: err, 65 | } 66 | for i := frames; i > 0; i-- { 67 | event.Frames = append(event.Frames, &cue.Frame{ 68 | Package: fmt.Sprintf("github.com/bobziuchkovski/cue/frame%d", i), 69 | Function: fmt.Sprintf("github.com/bobziuchkovski/cue/frame%d.function%d", i, i), 70 | File: fmt.Sprintf("/path/github.com/bobziuchkovski/cue/frame%d/file%d.go", i, i), 71 | Line: i, 72 | }) 73 | } 74 | return event 75 | } 76 | 77 | func testTime() time.Time { 78 | t, err := time.Parse(time.RFC822, time.RFC822) 79 | if err != nil { 80 | panic(err) 81 | } 82 | return t 83 | } 84 | -------------------------------------------------------------------------------- /internal/cuetest/http.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cuetest 22 | 23 | import ( 24 | "bufio" 25 | "bytes" 26 | "fmt" 27 | "io/ioutil" 28 | "net/http" 29 | "net/http/httputil" 30 | "sync" 31 | ) 32 | 33 | // HTTPRequestRecorder implements http.RoundTripper, capturing all requests 34 | // that are sent to it. 35 | type HTTPRequestRecorder struct { 36 | mu sync.Mutex 37 | requests []*http.Request 38 | } 39 | 40 | // NewHTTPRequestRecorder returns a new HTTPRequestRecorder instance. 41 | func NewHTTPRequestRecorder() *HTTPRequestRecorder { 42 | return &HTTPRequestRecorder{} 43 | } 44 | 45 | // ServeHTTP is implemented to satisfy the http.RoundTripper interface. 46 | func (rr *HTTPRequestRecorder) ServeHTTP(w http.ResponseWriter, req *http.Request) { 47 | dump, err := httputil.DumpRequest(req, true) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | buf := bytes.NewBuffer(dump) 53 | dupe, err := http.ReadRequest(bufio.NewReader(buf)) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | buf.Reset() 59 | buf.ReadFrom(req.Body) 60 | dupe.Body = ioutil.NopCloser(buf) 61 | 62 | rr.mu.Lock() 63 | defer rr.mu.Unlock() 64 | rr.requests = append(rr.requests, dupe) 65 | } 66 | 67 | // Requests returns a slice of the requests captured by the recorder. 68 | func (rr *HTTPRequestRecorder) Requests() []*http.Request { 69 | rr.mu.Lock() 70 | defer rr.mu.Unlock() 71 | return rr.requests 72 | } 73 | 74 | type failingHTTPTransport struct { 75 | succeedAfter int 76 | failCount int 77 | } 78 | 79 | // NewFailingHTTPTransport returns a http.RoundTripper that fails requests 80 | // until succeedAfter count have been submitted. Afterwards, it passes 81 | // requests to http.DefaultTransport. 82 | func NewFailingHTTPTransport(succeedAfter int) http.RoundTripper { 83 | return &failingHTTPTransport{ 84 | succeedAfter: succeedAfter, 85 | } 86 | } 87 | 88 | func (t *failingHTTPTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 89 | if t.failCount < t.succeedAfter { 90 | t.failCount++ 91 | err = fmt.Errorf("%d more failures before I pass the HTTP request", t.succeedAfter-t.failCount) 92 | return 93 | } 94 | return http.DefaultTransport.RoundTrip(req) 95 | } 96 | -------------------------------------------------------------------------------- /internal/cuetest/json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cuetest 22 | 23 | import ( 24 | "encoding/json" 25 | "net/http" 26 | "reflect" 27 | "strings" 28 | "testing" 29 | ) 30 | 31 | // ParseRequestJSON parses the request body as json, returning a 32 | // nested map[string]interface{} of the decoded contents. 33 | // If the content isn't well-formed, ParseRequestJSON panics. 34 | func ParseRequestJSON(req *http.Request) map[string]interface{} { 35 | j := make(map[string]interface{}) 36 | d := json.NewDecoder(req.Body) 37 | d.UseNumber() 38 | err := d.Decode(&j) 39 | if err != nil { 40 | panic(err) 41 | } 42 | return j 43 | } 44 | 45 | // ParseStringJSON parses the input jstr as json, returning a 46 | // nested map[string]interface{} of the decoded contents. 47 | // If the content isn't well-formed ParseStringJSON panics. 48 | func ParseStringJSON(jstr string) map[string]interface{} { 49 | j := make(map[string]interface{}) 50 | d := json.NewDecoder(strings.NewReader(jstr)) 51 | d.UseNumber() 52 | err := d.Decode(&j) 53 | if err != nil { 54 | panic(err) 55 | } 56 | return j 57 | } 58 | 59 | // NestedFetch treats j as a nested map[string]interface{} and attempts to 60 | // retrieve the value specified by path. It returns "!(MISSING)" if the 61 | // value is missing, or "!(NOTAKEY)" if part of the path exists but terminates 62 | // early at a value that isn't a key. 63 | func NestedFetch(j map[string]interface{}, path ...string) interface{} { 64 | for i, part := range path { 65 | v, present := j[part] 66 | if !present { 67 | return "!(MISSING)" 68 | } 69 | if i >= len(path)-1 { 70 | return v 71 | } 72 | sub, ok := v.(map[string]interface{}) 73 | if !ok { 74 | return "!(NOTAKEY)" 75 | } 76 | j = sub 77 | } 78 | return "!(MISSING)" 79 | } 80 | 81 | // NestedDelete treats j as a nested map[string]interface{} and attempts to 82 | // delete the value specified by the path. If does nothing if the path doesn't 83 | // correspond to a valid key. 84 | func NestedDelete(j map[string]interface{}, path ...string) { 85 | for i, part := range path { 86 | v, present := j[part] 87 | if !present { 88 | return 89 | } 90 | if i >= len(path)-1 { 91 | delete(j, part) 92 | return 93 | } 94 | sub, ok := v.(map[string]interface{}) 95 | if !ok { 96 | return 97 | } 98 | j = sub 99 | } 100 | } 101 | 102 | // NestedCompare treats input and expected as nested map[string]interface{} and 103 | // performs a deep comparison between them. If the maps aren't equal, it 104 | // calls t.Errorf with a pretty-printed comparison. 105 | func NestedCompare(t *testing.T, input map[string]interface{}, expected map[string]interface{}) { 106 | if !reflect.DeepEqual(input, expected) { 107 | prettyInput := prettyFormat(input) 108 | prettyExpected := prettyFormat(expected) 109 | t.Errorf(` 110 | Request JSON doesn't match expectations. 111 | 112 | Expected 113 | ======== 114 | %s 115 | 116 | Received 117 | ======== 118 | %s 119 | `, prettyExpected, prettyInput) 120 | } 121 | } 122 | 123 | func prettyFormat(j map[string]interface{}) string { 124 | bytes, err := json.MarshalIndent(j, "", " ") 125 | if err != nil { 126 | panic(err) 127 | } 128 | return string(bytes) 129 | } 130 | -------------------------------------------------------------------------------- /internal/cuetest/misc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cuetest 22 | 23 | import ( 24 | "github.com/bobziuchkovski/cue" 25 | "io" 26 | "time" 27 | ) 28 | 29 | // CloseCollector calls c.Close() if c implements the io.Closer interface. 30 | // If c.Close() doesn't return within 5 seconds, CloseCollector panics. 31 | func CloseCollector(c cue.Collector) { 32 | closer, ok := c.(io.Closer) 33 | if !ok { 34 | return 35 | } 36 | 37 | timer := time.AfterFunc(5*time.Second, func() { 38 | panic("Failed to close collector within 5 seconds") 39 | }) 40 | closer.Close() 41 | timer.Stop() 42 | } 43 | 44 | // ResetCue calls cue.Close(time.Minute). If that returns a non-nil result, 45 | // ResetCue panics. 46 | func ResetCue() { 47 | err := cue.Close(time.Minute) 48 | if err != nil { 49 | panic("Cue failed to reset within a minute") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/cuetest/net.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cuetest 22 | 23 | import ( 24 | "bytes" 25 | "crypto/tls" 26 | "net" 27 | "reflect" 28 | "sync" 29 | "testing" 30 | ) 31 | 32 | // NetRecorder is an interface representing a network listener/recorder. The 33 | // recorder stores all content sent to it. Recorders are created in an 34 | // unstarted state and must be explicitly started via the Start() method and 35 | // explicitly stopped via the Close() method once finished. 36 | type NetRecorder interface { 37 | // Address returns the address string for the recorder. 38 | Address() string 39 | 40 | // Contents returns the bytes that have been sent to the recorder. 41 | Contents() []byte 42 | 43 | // CheckByteContents checks if the bytes captured by the recorder match the 44 | // given expectation. If not, t.Errorf is called with a comparison. 45 | CheckByteContents(t *testing.T, expectation []byte) 46 | 47 | // CheckStringContents checks if the bytes captured by the recorder match 48 | // the given string expectation. If not, t.Errorf is called with a 49 | // comparison. 50 | CheckStringContents(t *testing.T, expectation string) 51 | 52 | // Start starts the recorder. 53 | Start() 54 | 55 | // Close stops the recorder and terminates any active connections. 56 | Close() error 57 | 58 | // Done returns a channel that blocks until the recorder is finished. 59 | Done() <-chan struct{} 60 | 61 | // Err returns the first error encountered by the recorder, if any. 62 | Err() error 63 | } 64 | 65 | type netRecorder struct { 66 | done chan struct{} 67 | cancel chan struct{} 68 | err *firstError 69 | 70 | startOnce sync.Once 71 | closeOnce sync.Once 72 | 73 | mu sync.Mutex 74 | network string 75 | address string 76 | enableTLS bool 77 | content []byte 78 | listener net.Listener 79 | } 80 | 81 | // NewTCPRecorder returns a NetRecorder that listens for TCP connections. 82 | func NewTCPRecorder() NetRecorder { 83 | return newNetRecorder("tcp", randomAddress(), false) 84 | } 85 | 86 | // NewTLSRecorder returns a NetRecorder that listens for TCP connections using 87 | // TLS transport encryption. 88 | func NewTLSRecorder() NetRecorder { 89 | return newNetRecorder("tcp", randomAddress(), true) 90 | } 91 | 92 | func newNetRecorder(network, address string, enableTLS bool) NetRecorder { 93 | return &netRecorder{ 94 | done: make(chan struct{}), 95 | cancel: make(chan struct{}), 96 | network: network, 97 | address: address, 98 | enableTLS: enableTLS, 99 | err: &firstError{}, 100 | } 101 | } 102 | 103 | func (nr *netRecorder) Start() { 104 | nr.startOnce.Do(func() { 105 | var err error 106 | var listener net.Listener 107 | 108 | if nr.enableTLS { 109 | cert, err := tls.LoadX509KeyPair("test.crt", "test.key") 110 | if err != nil { 111 | panic(err) 112 | } 113 | tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}} 114 | listener, err = tls.Listen(nr.network, nr.address, tlsConfig) 115 | } else { 116 | listener, err = net.Listen(nr.network, nr.address) 117 | } 118 | if err == nil { 119 | nr.listener = listener 120 | go nr.run() 121 | } else { 122 | panic(err) 123 | } 124 | }) 125 | } 126 | 127 | func (nr *netRecorder) Address() string { 128 | return nr.address 129 | } 130 | 131 | func (nr *netRecorder) Contents() []byte { 132 | <-nr.done 133 | 134 | nr.mu.Lock() 135 | defer nr.mu.Unlock() 136 | return nr.content 137 | } 138 | 139 | func (nr *netRecorder) CheckByteContents(t *testing.T, expectation []byte) { 140 | if !reflect.DeepEqual(nr.Contents(), expectation) { 141 | t.Errorf("Expected recorded content of %x but got %x instead", expectation, nr.Contents()) 142 | } 143 | } 144 | 145 | func (nr *netRecorder) CheckStringContents(t *testing.T, expectation string) { 146 | if string(nr.Contents()) != expectation { 147 | t.Errorf("Expected recorded content of %q but got %q instead", expectation, nr.Contents()) 148 | } 149 | } 150 | 151 | func (nr *netRecorder) Done() <-chan struct{} { 152 | return nr.done 153 | } 154 | 155 | func (nr *netRecorder) Err() error { 156 | <-nr.done 157 | return nr.err.Error() 158 | } 159 | 160 | func (nr *netRecorder) Close() error { 161 | nr.closeOnce.Do(func() { 162 | close(nr.cancel) 163 | if nr.listener != nil { 164 | nr.listener.Close() 165 | <-nr.done 166 | } 167 | }) 168 | 169 | return nr.err.Error() 170 | } 171 | 172 | func (nr *netRecorder) run() { 173 | conn, err := nr.listener.Accept() 174 | if err != nil { 175 | nr.err.Set(err) 176 | close(nr.done) 177 | return 178 | } 179 | 180 | go func(conn net.Conn) { 181 | <-nr.cancel 182 | nr.err.Set(conn.Close()) 183 | }(conn) 184 | 185 | var buf bytes.Buffer 186 | _, err = buf.ReadFrom(conn) 187 | nr.err.Set(err) 188 | 189 | nr.mu.Lock() 190 | defer nr.mu.Unlock() 191 | nr.content = buf.Bytes() 192 | close(nr.done) 193 | } 194 | 195 | type firstError struct { 196 | mu sync.Mutex 197 | err error 198 | } 199 | 200 | func (se *firstError) Error() error { 201 | se.mu.Lock() 202 | defer se.mu.Unlock() 203 | return se.err 204 | } 205 | 206 | func (se *firstError) Set(err error) { 207 | se.mu.Lock() 208 | defer se.mu.Unlock() 209 | if se.err == nil { 210 | se.err = err 211 | } 212 | } 213 | 214 | func randomAddress() string { 215 | l, err := net.Listen("tcp", "localhost:0") 216 | defer l.Close() 217 | 218 | if err != nil { 219 | panic(err) 220 | } 221 | return l.Addr().String() 222 | } 223 | -------------------------------------------------------------------------------- /level.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | // OFF, FATAL, ERROR, WARN, INFO, and DEBUG are logging Level constants. 24 | const ( 25 | OFF Level = iota 26 | FATAL 27 | ERROR 28 | WARN 29 | INFO 30 | DEBUG 31 | ) 32 | 33 | // Level represents the severity level for a logged event. Events are only 34 | // generated and collected if their severity level is within the threshold 35 | // level for one or more registered Collectors. Calling Logger.Info, for 36 | // example, will only generate an event if a Collector is registered at the 37 | // INFO or DEBUG threshold levels. 38 | type Level uint 39 | 40 | // String returns the level's name. 41 | func (l Level) String() string { 42 | switch l { 43 | case DEBUG: 44 | return "DEBUG" 45 | case INFO: 46 | return "INFO" 47 | case WARN: 48 | return "WARN" 49 | case ERROR: 50 | return "ERROR" 51 | case FATAL: 52 | return "FATAL" 53 | case OFF: 54 | return "OFF" 55 | default: 56 | return "INVALID LEVEL" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /level_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "testing" 25 | ) 26 | 27 | func TestLevelString(t *testing.T) { 28 | if OFF.String() != "OFF" { 29 | t.Errorf("OFF.String value is incorrect. Expected %q but received %q instead", "OFF", OFF.String()) 30 | } 31 | if DEBUG.String() != "DEBUG" { 32 | t.Errorf("DEBUG.String value is incorrect. Expected %q but received %q instead", "DEBUG", DEBUG.String()) 33 | } 34 | if INFO.String() != "INFO" { 35 | t.Errorf("INFO.String value is incorrect. Expected %q but received %q instead", "INFO", INFO.String()) 36 | } 37 | if WARN.String() != "WARN" { 38 | t.Errorf("WARN.String value is incorrect. Expected %q but received %q instead", "WARN", WARN.String()) 39 | } 40 | if ERROR.String() != "ERROR" { 41 | t.Errorf("ERROR.String value is incorrect. Expected %q but received %q instead", "ERROR", ERROR.String()) 42 | } 43 | if FATAL.String() != "FATAL" { 44 | t.Errorf("FATAL.String value is incorrect. Expected %q but received %q instead", "FATAL", FATAL.String()) 45 | } 46 | if Level(42).String() != "INVALID LEVEL" { 47 | t.Error("Expected to see INVALID LEVEL for bogus level") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /panic.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "runtime" 25 | ) 26 | 27 | // maxPanicDepth is the maximum number of frames to search when locating the 28 | // call site that triggered panic(). 8 frames is arbitrary, but it's 29 | // relatively safe to assume that panic adds less than 8 frames to the stack. 30 | // On amd64 with go 1.6, panic adds 2 frames to the stack. 31 | const maxPanicDepth = 8 32 | 33 | var _, _, _, canDetect = runtime.Caller(0) 34 | 35 | func doPanic(cause interface{}) { 36 | panic(cause) 37 | } 38 | 39 | // Detect whether the current stack is a panic caused by us. 40 | func ourPanic() bool { 41 | if !canDetect { 42 | return false 43 | } 44 | 45 | framebuf := make([]uintptr, maxPanicDepth) 46 | copied := runtime.Callers(0, framebuf) 47 | framebuf = framebuf[:copied] 48 | for _, pc := range framebuf { 49 | if frameForPC(pc).Function == "github.com/bobziuchkovski/cue.doPanic" { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | -------------------------------------------------------------------------------- /panic_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "testing" 25 | ) 26 | 27 | func TestOurPanic(t *testing.T) { 28 | builtinCause := "built-in" 29 | panicCause := "logger panic" 30 | panicfCause := "logger panicf" 31 | 32 | regularPanic := func() { panic(builtinCause) } 33 | logPanic := func() { NewLogger("logger panic").Panic(panicCause, "logger panic") } 34 | logPanicf := func() { NewLogger("logger panicf").Panicf(panicfCause, "logger %s", "panicf") } 35 | if recoverAndCheckOurPanic(regularPanic) { 36 | t.Error("Regular built-in panic incorrectly detected as our own.") 37 | } 38 | if !recoverAndCheckOurPanic(logPanic) { 39 | t.Error("Logger panic not detected as our own") 40 | } 41 | if !recoverAndCheckOurPanic(logPanicf) { 42 | t.Error("Logger panicf not detected as our own") 43 | } 44 | } 45 | 46 | func recoverAndCheckOurPanic(fn func()) (ours bool) { 47 | defer func() { 48 | recover() 49 | ours = ourPanic() 50 | }() 51 | fn() 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "fmt" 25 | "sync" 26 | "time" 27 | ) 28 | 29 | type capturingCollector struct { 30 | captured []*Event 31 | cond *sync.Cond 32 | mu sync.Mutex 33 | } 34 | 35 | func newCapturingCollector() *capturingCollector { 36 | c := &capturingCollector{} 37 | c.cond = sync.NewCond(&c.mu) 38 | return c 39 | } 40 | 41 | func (c *capturingCollector) Collect(event *Event) error { 42 | c.mu.Lock() 43 | defer c.mu.Unlock() 44 | c.captured = append(c.captured, event) 45 | c.cond.Broadcast() 46 | return nil 47 | } 48 | 49 | func (c *capturingCollector) Captured() []*Event { 50 | c.mu.Lock() 51 | defer c.mu.Unlock() 52 | dup := make([]*Event, len(c.captured)) 53 | for i, event := range c.captured { 54 | dup[i] = event 55 | } 56 | return dup 57 | } 58 | 59 | func (c *capturingCollector) WaitCaptured(count int, maxWait time.Duration) { 60 | finished := make(chan struct{}) 61 | go c.waitAsync(count, finished) 62 | 63 | select { 64 | case <-finished: 65 | return 66 | case <-time.After(maxWait): 67 | panic("WaitCaptured timed-out waiting for events") 68 | } 69 | } 70 | 71 | func (c *capturingCollector) waitAsync(count int, finished chan struct{}) { 72 | c.mu.Lock() 73 | defer c.mu.Unlock() 74 | for len(c.captured) != count { 75 | c.cond.Wait() 76 | } 77 | close(finished) 78 | } 79 | 80 | func (c *capturingCollector) String() string { 81 | return "capturingCollector()" 82 | } 83 | 84 | type blockingCollector struct { 85 | collector Collector 86 | unblocked chan struct{} 87 | } 88 | 89 | func newBlockingCollector(c Collector) *blockingCollector { 90 | return &blockingCollector{ 91 | collector: c, 92 | unblocked: make(chan struct{}), 93 | } 94 | } 95 | 96 | func (c *blockingCollector) Unblock() { 97 | close(c.unblocked) 98 | } 99 | 100 | func (c *blockingCollector) Collect(event *Event) error { 101 | <-c.unblocked 102 | return c.collector.Collect(event) 103 | } 104 | 105 | func (c *blockingCollector) String() string { 106 | return fmt.Sprintf("blockingCollector(target=%s)", c.collector) 107 | } 108 | 109 | type failingCollector struct { 110 | collector Collector 111 | succeedAfter int 112 | failCount int 113 | } 114 | 115 | func newFailingCollector(c Collector, succeedAfter int) *failingCollector { 116 | return &failingCollector{ 117 | collector: c, 118 | succeedAfter: succeedAfter, 119 | } 120 | } 121 | 122 | func (c *failingCollector) Collect(event *Event) error { 123 | if c.failCount < c.succeedAfter { 124 | c.failCount++ 125 | return fmt.Errorf("%d more failures before I pass the event to my collector", c.succeedAfter-c.failCount) 126 | } 127 | return c.collector.Collect(event) 128 | } 129 | 130 | func (c *failingCollector) String() string { 131 | return fmt.Sprintf("failingCollector(target=%s)", c.collector) 132 | } 133 | 134 | type panickingCollector struct { 135 | collector Collector 136 | succeedAfter int 137 | panicCount int 138 | } 139 | 140 | func newPanickingCollector(c Collector, succeedAfter int) *panickingCollector { 141 | return &panickingCollector{ 142 | collector: c, 143 | succeedAfter: succeedAfter, 144 | } 145 | } 146 | 147 | func (c *panickingCollector) Collect(event *Event) error { 148 | if c.panicCount < c.succeedAfter { 149 | c.panicCount++ 150 | panic(fmt.Sprintf("%d more failures before I pass the event to my collector", c.succeedAfter-c.panicCount)) 151 | } 152 | return c.collector.Collect(event) 153 | } 154 | 155 | func (c *panickingCollector) String() string { 156 | return fmt.Sprintf("panickingCollector(target=%s)", c.collector) 157 | } 158 | 159 | type closingCollector struct { 160 | cond *sync.Cond 161 | mu sync.Mutex 162 | collector Collector 163 | closed bool 164 | } 165 | 166 | func newClosingCollector(c Collector) *closingCollector { 167 | closing := &closingCollector{ 168 | collector: c, 169 | } 170 | closing.cond = sync.NewCond(&closing.mu) 171 | return closing 172 | } 173 | 174 | func (c *closingCollector) Collect(event *Event) error { 175 | return c.collector.Collect(event) 176 | } 177 | 178 | func (c *closingCollector) Close() error { 179 | c.mu.Lock() 180 | defer c.mu.Unlock() 181 | 182 | c.closed = true 183 | c.cond.Broadcast() 184 | return nil 185 | } 186 | 187 | func (c *closingCollector) Closed() bool { 188 | c.mu.Lock() 189 | defer c.mu.Unlock() 190 | 191 | return c.closed 192 | } 193 | 194 | func (c *closingCollector) WaitClosed(maxWait time.Duration) { 195 | finished := make(chan struct{}) 196 | go c.waitAsync(finished) 197 | 198 | select { 199 | case <-finished: 200 | return 201 | case <-time.After(maxWait): 202 | panic("WaitClosed timed-out waiting for Close() to be called") 203 | } 204 | } 205 | 206 | func (c *closingCollector) waitAsync(finished chan struct{}) { 207 | c.mu.Lock() 208 | defer c.mu.Unlock() 209 | for !c.closed { 210 | c.cond.Wait() 211 | } 212 | close(finished) 213 | } 214 | 215 | func callWithRecover(fn func()) { 216 | defer func() { 217 | recover() 218 | }() 219 | fn() 220 | } 221 | 222 | func callWithLoggerRecover(fn func(), logger Logger, message string) { 223 | defer logger.Recover(message) 224 | fn() 225 | } 226 | 227 | func callWithLoggerReportRecovery(fn func(), logger Logger, message string) { 228 | defer func() { 229 | cause := recover() 230 | logger.ReportRecovery(cause, message) 231 | }() 232 | fn() 233 | } 234 | 235 | func resetCue() { 236 | err := Close(time.Minute) 237 | if err != nil { 238 | panic("Cue failed to reset within a minute") 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | // Version records the cue package version. 24 | var Version = struct { 25 | Major int 26 | Minor int 27 | Patch int 28 | }{0, 8, 0} 29 | -------------------------------------------------------------------------------- /worker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cue 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "io" 27 | "math" 28 | "sync" 29 | "sync/atomic" 30 | "time" 31 | ) 32 | 33 | var errDrops = errors.New("events dropped due to full buffer") 34 | 35 | const ( 36 | // Number of collector.Collect() retries before failing an event. 37 | sendRetries = 2 38 | 39 | // Maximum time to delay between collector.Collect() attempts for a 40 | // degraded collector. The backoff is exponentual up to this limit. 41 | maxDelay = 5 * time.Minute 42 | ) 43 | 44 | type worker interface { 45 | Send(event *Event) 46 | Terminate(flush bool) 47 | } 48 | 49 | func newWorker(c Collector, bufsize int) worker { 50 | if bufsize == 0 { 51 | return newSyncWorker(c) 52 | } 53 | return newAsyncWorker(c, bufsize) 54 | } 55 | 56 | type syncWorker struct { 57 | mu sync.Mutex 58 | collector Collector 59 | terminated bool 60 | drops uint64 61 | } 62 | 63 | func newSyncWorker(c Collector) worker { 64 | return &syncWorker{ 65 | collector: c, 66 | } 67 | } 68 | 69 | func (w *syncWorker) Send(e *Event) { 70 | w.mu.Lock() 71 | defer w.mu.Unlock() 72 | if !w.terminated { 73 | w.sendEvent(e) 74 | } 75 | } 76 | 77 | func (w *syncWorker) Terminate(flush bool) { 78 | w.mu.Lock() 79 | defer w.mu.Unlock() 80 | 81 | closeCollector(w.collector) 82 | w.terminated = true 83 | } 84 | 85 | func (w *syncWorker) sendEvent(event *Event) { 86 | err := sendWithRetries(w.collector, event, sendRetries) 87 | if err == nil { 88 | return 89 | } 90 | w.drops++ 91 | handleDegradation(w.collector, err, w.drops) 92 | } 93 | 94 | type asyncWorker struct { 95 | // Drops is accessed via atomic operations. It's the first field to ensure 96 | // 64-bit alignment. See the sync/atomic docs for details. 97 | drops uint64 98 | 99 | collector Collector 100 | queue chan *Event 101 | terminate chan bool 102 | finished chan struct{} 103 | lastdrops uint64 104 | } 105 | 106 | func newAsyncWorker(c Collector, bufsize int) worker { 107 | w := &asyncWorker{ 108 | collector: c, 109 | queue: make(chan *Event, bufsize), 110 | terminate: make(chan bool, 1), 111 | finished: make(chan struct{}), 112 | } 113 | go w.run() 114 | return w 115 | } 116 | 117 | func (w *asyncWorker) Send(e *Event) { 118 | select { 119 | case w.queue <- e: 120 | // No-op...event is queued 121 | default: 122 | atomic.AddUint64(&w.drops, 1) 123 | } 124 | } 125 | 126 | func (w *asyncWorker) run() { 127 | for { 128 | select { 129 | case event := <-w.queue: 130 | w.handleDrops() 131 | if event != nil { 132 | w.sendEvent(event) 133 | } 134 | case flush := <-w.terminate: 135 | w.cleanup(flush) 136 | close(w.finished) 137 | return 138 | } 139 | } 140 | } 141 | 142 | func (w *asyncWorker) Terminate(flush bool) { 143 | close(w.queue) 144 | w.terminate <- flush 145 | close(w.terminate) 146 | <-w.finished 147 | } 148 | 149 | func (w *asyncWorker) cleanup(flush bool) { 150 | if flush { 151 | for event := range w.queue { 152 | w.sendEvent(event) 153 | } 154 | } 155 | closeCollector(w.collector) 156 | w.queue = nil 157 | } 158 | 159 | func (w *asyncWorker) sendEvent(event *Event) { 160 | err := sendWithRetries(w.collector, event, sendRetries) 161 | if err == nil { 162 | return 163 | } 164 | drops := atomic.AddUint64(&w.drops, 1) 165 | handleDegradation(w.collector, err, drops) 166 | w.lastdrops = drops 167 | } 168 | 169 | func (w *asyncWorker) handleDrops() { 170 | drops := atomic.LoadUint64(&w.drops) 171 | if drops > w.lastdrops { 172 | handleDegradation(w.collector, errDrops, drops) 173 | w.lastdrops = drops 174 | } 175 | } 176 | 177 | func sendWithRetries(c Collector, event *Event, retries int) error { 178 | defer recoverCollector(c) 179 | var collectorErr error 180 | for attempt := 0; attempt <= retries; attempt++ { 181 | err := c.Collect(event) 182 | if err == nil { 183 | return nil 184 | } 185 | if collectorErr == nil { 186 | collectorErr = err 187 | } 188 | } 189 | return collectorErr 190 | } 191 | 192 | func handleDegradation(c Collector, err error, drops uint64) { 193 | defer recoverCollector(c) 194 | setDegraded(c, true) 195 | go internalLogger.WithFields(Fields{ 196 | "drops": drops, 197 | }).Errorf(err, "Collector has entered a degraded state: %s", c) 198 | 199 | ensureErrorSent(c, err, drops) 200 | 201 | setDegraded(c, false) 202 | go internalLogger.Warnf("Collector has recovered from a degraded stated: %s", c) 203 | } 204 | 205 | func ensureErrorSent(c Collector, err error, drops uint64) { 206 | startTime := time.Now() 207 | attempt := 0 208 | for { 209 | attempt++ 210 | time.Sleep(backoff(attempt)) 211 | 212 | ctx := internalContext.WithFields(Fields{ 213 | "attempts": attempt, 214 | "drops": drops, 215 | }) 216 | event := newEventf(ctx, ERROR, err, "The current collector, %s, has been in a degraded state since %s. Delivery of this message has been attempted %d times", c, startTime.Format(time.Stamp), attempt) 217 | if c.Collect(event) == nil { 218 | return 219 | } 220 | } 221 | } 222 | 223 | func closeCollector(c Collector) { 224 | closer, ok := c.(io.Closer) 225 | if !ok { 226 | return 227 | } 228 | internalLogger.Errorf(closer.Close(), "Failed to close collector %s", c) 229 | } 230 | 231 | func recoverCollector(c Collector) { 232 | cause := recover() 233 | if cause == nil { 234 | return 235 | } 236 | 237 | go func() { 238 | dispose(c) 239 | message := fmt.Sprintf("Recovered from collector panic. Collector has been disposed: %s", c) 240 | internalLogger.ReportRecovery(cause, message) 241 | }() 242 | } 243 | 244 | func backoff(attempt int) time.Duration { 245 | exp := math.Pow(2, float64(attempt)) 246 | if math.IsNaN(exp) || math.IsInf(exp, 1) || math.IsInf(exp, -1) { 247 | return maxDelay 248 | } 249 | 250 | delay := time.Millisecond * time.Duration(exp) 251 | if delay > maxDelay { 252 | delay = maxDelay 253 | } 254 | return delay 255 | } 256 | --------------------------------------------------------------------------------