├── .travis.yml ├── LICENSE ├── README.md ├── example └── example.go └── raven ├── raven.go └── raven_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.2 5 | - 1.3 6 | - tip 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Kamil Kisiel 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | raven-go 2 | ======== 3 | 4 | A Go library for communicating with [Sentry][sentry]. 5 | 6 | [sentry]: http://www.github.com/dcramer/sentry 7 | 8 | [![Build Status](https://travis-ci.org/kisielk/raven-go.png?branch=master)](http://travis-ci.org/kisielk/raven-go) 9 | 10 | See [godoc.org](http://godoc.org/github.com/kisielk/raven-go/raven) for the API documentation. 11 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/kisielk/raven-go/raven" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func main() { 11 | 12 | var dsn string 13 | if len(os.Args) >= 2 { 14 | dsn = strings.Join(os.Args[1:], " ") 15 | } else { 16 | dsn = os.Getenv("SENTRY_DSN") 17 | } 18 | 19 | if dsn == "" { 20 | fmt.Printf("Error: No configuration detected!\n") 21 | fmt.Printf("You must either pass a DSN to the command, or set the SENTRY_DSN environment variable\n") 22 | return 23 | } 24 | 25 | fmt.Printf("Using DSN configuration:\n %v\n", dsn) 26 | client, err := raven.NewClient(dsn) 27 | 28 | if err != nil { 29 | fmt.Printf("could not connect: %v", dsn) 30 | return 31 | } 32 | 33 | fmt.Printf("Sending a test message...\n") 34 | id, err := client.CaptureMessage("This is a test message generated using ``goraven test``") 35 | 36 | if err != nil { 37 | fmt.Printf("failed: %v\n", err) 38 | return 39 | } 40 | 41 | fmt.Printf("Message captured, id: %v", id) 42 | } 43 | -------------------------------------------------------------------------------- /raven/raven.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Package raven is a client and library for sending messages and exceptions to Sentry: http://getsentry.com 4 | 5 | Usage: 6 | 7 | Create a new client using the NewClient() function. The value for the DSN parameter can be obtained 8 | from the project page in the Sentry web interface. After the client has been created use the CaptureMessage 9 | method to send messages to the server. 10 | 11 | client, err := raven.NewClient(dsn) 12 | ... 13 | id, err := client.CaptureMessage("some text") 14 | 15 | If you want to have more finegrained control over the send event, you can create the event instance yourself 16 | 17 | client.Capture(&raven.Event{Message: "Some Text", Logger:"auth"}) 18 | 19 | */ 20 | package raven 21 | 22 | import ( 23 | "bytes" 24 | "compress/zlib" 25 | "crypto/rand" 26 | "encoding/base64" 27 | "encoding/hex" 28 | "encoding/json" 29 | "errors" 30 | "fmt" 31 | "net" 32 | "net/http" 33 | "net/url" 34 | "path" 35 | "runtime" 36 | "strconv" 37 | "strings" 38 | "time" 39 | ) 40 | 41 | type Client struct { 42 | URL *url.URL 43 | PublicKey string 44 | SecretKey string 45 | Project string 46 | httpClient *http.Client 47 | } 48 | 49 | type Frame struct { 50 | Filename string `json:"filename"` 51 | LineNumber int `json:"lineno"` 52 | FilePath string `json:"abs_path"` 53 | Function string `json:"function"` 54 | Module string `json:"module"` 55 | } 56 | 57 | type Stacktrace struct { 58 | Frames []Frame `json:"frames"` 59 | } 60 | 61 | func generateStacktrace() Stacktrace { 62 | var stacktrace Stacktrace 63 | maxDepth := 10 64 | // Start on depth 1 to avoid stack for generateStacktrace 65 | for depth := 1; depth < maxDepth; depth++ { 66 | pc, filePath, line, ok := runtime.Caller(depth) 67 | if !ok { 68 | break 69 | } 70 | f := runtime.FuncForPC(pc) 71 | if strings.Contains(f.Name(), "runtime") { 72 | // Stop when reaching runtime 73 | break 74 | } 75 | if strings.Contains(f.Name(), "raven.Client") { 76 | // Skip internal calls 77 | continue 78 | } 79 | functionName := f.Name() 80 | var moduleName string 81 | if strings.Contains(f.Name(), "(") { 82 | components := strings.SplitN(f.Name(), ".(", 2) 83 | functionName = "(" + components[1] 84 | moduleName = components[0] 85 | } 86 | fileName := path.Base(filePath) 87 | frame := Frame{Filename: fileName, LineNumber: line, FilePath: filePath, 88 | Function: functionName, Module: moduleName} 89 | stacktrace.Frames = append(stacktrace.Frames, frame) 90 | } 91 | return stacktrace 92 | } 93 | 94 | type Event struct { 95 | EventId string `json:"event_id"` 96 | Project string `json:"project"` 97 | Message string `json:"message"` 98 | Timestamp string `json:"timestamp"` 99 | Level string `json:"level"` 100 | Logger string `json:"logger"` 101 | Culprit string `json:"culprit"` 102 | Stacktrace Stacktrace `json:"stacktrace"` 103 | } 104 | 105 | type sentryResponse struct { 106 | ResultId string `json:"result_id"` 107 | } 108 | 109 | // Template for the X-Sentry-Auth header 110 | const xSentryAuthTemplate = "Sentry sentry_version=2.0, sentry_client=raven-go/0.1, sentry_timestamp=%v, sentry_key=%v" 111 | 112 | // An iso8601 timestamp without the timezone. This is the format Sentry expects. 113 | const iso8601 = "2006-01-02T15:04:05" 114 | 115 | const defaultTimeout = 3 * time.Second 116 | 117 | // NewClient creates a new client for a server identified by the given dsn 118 | // A dsn is a string in the form: 119 | // {PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}/{PATH}{PROJECT_ID} 120 | // eg: 121 | // http://abcd:efgh@sentry.example.com/sentry/project1 122 | func NewClient(dsn string) (client *Client, err error) { 123 | u, err := url.Parse(dsn) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | basePath := path.Dir(u.Path) 129 | project := path.Base(u.Path) 130 | 131 | if u.User == nil { 132 | return nil, fmt.Errorf("the DSN must contain a public and secret key") 133 | } 134 | publicKey := u.User.Username() 135 | secretKey, keyIsSet := u.User.Password() 136 | if !keyIsSet { 137 | return nil, fmt.Errorf("the DSN must contain a secret key") 138 | } 139 | 140 | u.Path = basePath 141 | 142 | check := func(req *http.Request, via []*http.Request) error { 143 | fmt.Printf("%+v", req) 144 | return nil 145 | } 146 | 147 | httpConnectTimeout := defaultTimeout 148 | httpReadWriteTimeout := defaultTimeout 149 | if st := u.Query().Get("timeout"); st != "" { 150 | if timeout, err := strconv.Atoi(st); err == nil { 151 | httpConnectTimeout = time.Duration(timeout) * time.Second 152 | httpReadWriteTimeout = time.Duration(timeout) * time.Second 153 | } else { 154 | return nil, fmt.Errorf("Timeout should have an Integer argument") 155 | } 156 | } 157 | 158 | transport := &transport{ 159 | httpTransport: &http.Transport{ 160 | Dial: timeoutDialer(httpConnectTimeout), 161 | Proxy: http.ProxyFromEnvironment, 162 | }, timeout: httpReadWriteTimeout} 163 | httpClient := &http.Client{ 164 | Transport: transport, 165 | CheckRedirect: check, 166 | } 167 | return &Client{URL: u, PublicKey: publicKey, SecretKey: secretKey, httpClient: httpClient, Project: project}, nil 168 | } 169 | 170 | // CaptureMessage sends a message to the Sentry server. 171 | // It returns the Sentry event ID or an empty string and any error that occurred. 172 | func (client Client) CaptureMessage(message ...string) (string, error) { 173 | ev := Event{Message: strings.Join(message, " ")} 174 | sentryErr := client.Capture(&ev) 175 | 176 | if sentryErr != nil { 177 | return "", sentryErr 178 | } 179 | return ev.EventId, nil 180 | } 181 | 182 | // CaptureMessagef is similar to CaptureMessage except it is using Printf to format the args in 183 | // to the given format string. 184 | func (client Client) CaptureMessagef(format string, args ...interface{}) (string, error) { 185 | return client.CaptureMessage(fmt.Sprintf(format, args...)) 186 | } 187 | 188 | // Capture sends the given event to Sentry. 189 | // Fields which are left blank are populated with default values. 190 | func (client Client) Capture(ev *Event) error { 191 | // Fill in defaults 192 | ev.Project = client.Project 193 | if ev.EventId == "" { 194 | eventId, err := uuid4() 195 | if err != nil { 196 | return err 197 | } 198 | ev.EventId = eventId 199 | } 200 | if ev.Level == "" { 201 | ev.Level = "error" 202 | } 203 | if ev.Logger == "" { 204 | ev.Logger = "root" 205 | } 206 | if ev.Timestamp == "" { 207 | now := time.Now().UTC() 208 | ev.Timestamp = now.Format(iso8601) 209 | } 210 | 211 | if len(ev.Stacktrace.Frames) == 0 { 212 | ev.Stacktrace = generateStacktrace() 213 | } 214 | 215 | buf, err := encode(ev) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | // Send 221 | timestamp, err := time.Parse(iso8601, ev.Timestamp) 222 | if err != nil { 223 | return err 224 | } 225 | 226 | err = client.send(buf, timestamp) 227 | if err != nil { 228 | return err 229 | } 230 | 231 | return nil 232 | } 233 | 234 | // sends a packet to the sentry server with a given timestamp 235 | func (client Client) send(packet []byte, timestamp time.Time) (err error) { 236 | apiURL := *client.URL 237 | apiURL.Path = path.Join(apiURL.Path, "/api/"+client.Project+"/store") 238 | apiURL.Path += "/" 239 | location := apiURL.String() 240 | 241 | buf := bytes.NewBuffer(packet) 242 | req, err := http.NewRequest("POST", location, buf) 243 | if err != nil { 244 | return err 245 | } 246 | 247 | authHeader := fmt.Sprintf(xSentryAuthTemplate, timestamp.Unix(), client.PublicKey) 248 | req.Header.Add("X-Sentry-Auth", authHeader) 249 | req.Header.Add("Content-Type", "application/octet-stream") 250 | req.Header.Add("Connection", "close") 251 | req.Header.Add("Accept-Encoding", "identity") 252 | 253 | resp, err := client.httpClient.Do(req) 254 | 255 | if err != nil { 256 | return err 257 | } 258 | 259 | defer resp.Body.Close() 260 | 261 | switch resp.StatusCode { 262 | case 200: 263 | return nil 264 | default: 265 | return errors.New(resp.Status) 266 | } 267 | } 268 | 269 | func uuid4() (string, error) { 270 | //TODO: Verify this algorithm or use an external library 271 | uuid := make([]byte, 16) 272 | n, err := rand.Read(uuid) 273 | if n != len(uuid) || err != nil { 274 | return "", err 275 | } 276 | uuid[8] = 0x80 277 | uuid[4] = 0x40 278 | 279 | return hex.EncodeToString(uuid), nil 280 | } 281 | 282 | func timeoutDialer(cTimeout time.Duration) func(net, addr string) (c net.Conn, err error) { 283 | return func(netw, addr string) (net.Conn, error) { 284 | conn, err := net.DialTimeout(netw, addr, cTimeout) 285 | if err != nil { 286 | return nil, err 287 | } 288 | return conn, nil 289 | } 290 | } 291 | 292 | // A custom http.Transport which allows us to put a timeout on each request. 293 | type transport struct { 294 | httpTransport *http.Transport 295 | timeout time.Duration 296 | } 297 | 298 | // Make use of Go 1.1's CancelRequest to close an outgoing connection if it 299 | // took longer than [timeout] to get a response. 300 | func (T *transport) RoundTrip(req *http.Request) (*http.Response, error) { 301 | timer := time.AfterFunc(T.timeout, func() { 302 | T.httpTransport.CancelRequest(req) 303 | }) 304 | defer timer.Stop() 305 | return T.httpTransport.RoundTrip(req) 306 | } 307 | 308 | func encode(ev *Event) ([]byte, error) { 309 | buf := new(bytes.Buffer) 310 | b64Encoder := base64.NewEncoder(base64.StdEncoding, buf) 311 | writer := zlib.NewWriter(b64Encoder) 312 | jsonEncoder := json.NewEncoder(writer) 313 | 314 | if err := jsonEncoder.Encode(ev); err != nil { 315 | return nil, err 316 | } 317 | err := writer.Close() 318 | if err != nil { 319 | return nil, err 320 | } 321 | 322 | if err := b64Encoder.Close(); err != nil { 323 | return nil, err 324 | } 325 | return buf.Bytes(), nil 326 | } 327 | -------------------------------------------------------------------------------- /raven/raven_test.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | import ( 4 | "compress/zlib" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "path" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func BuildSentryDSN(baseUrl, publicKey, secretKey, project, sentryPath string) string { 18 | u, _ := url.Parse(baseUrl) 19 | u.User = url.UserPassword(publicKey, secretKey) 20 | u.Path = path.Join(sentryPath, project) 21 | return u.String() 22 | } 23 | 24 | func GetServer() *httptest.Server { 25 | return httptest.NewServer(http.HandlerFunc( 26 | func(w http.ResponseWriter, req *http.Request) { 27 | fmt.Fprint(w, "hello") 28 | })) 29 | } 30 | 31 | func GetClient(server *httptest.Server) *Client { 32 | publicKey := "abcd" 33 | secretKey := "efgh" 34 | project := "1" 35 | sentryPath := "/sentry/path" 36 | 37 | // Build the client 38 | client, err := NewClient(BuildSentryDSN(server.URL, publicKey, secretKey, project, sentryPath)) 39 | if err != nil { 40 | panic(fmt.Sprintf("failed to make client: %s", err)) 41 | } 42 | return client 43 | } 44 | 45 | func TestClientSetup(t *testing.T) { 46 | publicKey := "abcd" 47 | secretKey := "efgh" 48 | project := "1" 49 | sentryPath := "/sentry/path" 50 | server := GetServer() 51 | defer server.Close() 52 | client := GetClient(server) 53 | 54 | // Test the client is set up correctly 55 | if client.PublicKey != publicKey { 56 | t.Logf("bad public key: got %s, want %s", client.PublicKey, publicKey) 57 | t.Fail() 58 | } 59 | if client.SecretKey != secretKey { 60 | t.Logf("bad public key: got %s, want %s", client.PublicKey, publicKey) 61 | t.Fail() 62 | } 63 | if client.Project != project { 64 | t.Logf("bad project: got %s, want %s", client.Project, project) 65 | t.Fail() 66 | } 67 | if client.URL.Path != sentryPath { 68 | t.Logf("bad path: got %s, want %s", client.URL.Path, sentryPath) 69 | t.Fail() 70 | } 71 | } 72 | 73 | func TestCaptureMessage(t *testing.T) { 74 | server := GetServer() 75 | defer server.Close() 76 | client := GetClient(server) 77 | _, err := client.CaptureMessage("test message") 78 | if err != nil { 79 | t.Logf("CaptureMessage failed: %s", err) 80 | t.Fail() 81 | } 82 | } 83 | 84 | func TestCapture(t *testing.T) { 85 | server := GetServer() 86 | defer server.Close() 87 | client := GetClient(server) 88 | 89 | // Send the message 90 | testEvent := func(ev *Event) { 91 | err := client.Capture(ev) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | // All fields must be set 96 | if ev.EventId == "" { 97 | t.Error("EventId must not be empty.") 98 | } 99 | if ev.Project == "" { 100 | t.Error("Project must not be empty.") 101 | } 102 | if ev.Timestamp == "" { 103 | t.Error("Timestamp must not be empty.") 104 | } 105 | if ev.Level == "" { 106 | t.Error("Level must not be empty.") 107 | } 108 | if ev.Logger == "" { 109 | t.Error("Logger must not be empty.") 110 | } 111 | if fmt.Sprintf("test.%s.%s", ev.Logger, ev.Level) != ev.Message { 112 | t.Errorf("Expected message to match error and logger %s == test.%s.%s", ev.Message, ev.Logger, ev.Level) 113 | } 114 | } 115 | 116 | testEvent(&Event{Message: "test.root.error"}) 117 | testEvent(&Event{Message: "test.root.warn", Level: "warn"}) 118 | testEvent(&Event{Message: "test.auth.error", Logger: "auth"}) 119 | testEvent(&Event{Message: "test.root.error", Timestamp: "2013-10-17T11:25:59"}) 120 | testEvent(&Event{Message: "test.root.error", EventId: "1234-34567-8912-124123"}) 121 | testEvent(&Event{Message: "test.auth.info", Level: "info", Logger: "auth"}) 122 | } 123 | 124 | func TestTimeout(t *testing.T) { 125 | server := httptest.NewServer(http.HandlerFunc( 126 | func(w http.ResponseWriter, req *http.Request) { 127 | time.Sleep(3100 * time.Millisecond) 128 | fmt.Fprint(w, "hello") 129 | })) 130 | defer server.Close() 131 | client := GetClient(server) 132 | 133 | _, err := client.CaptureMessage("Test message") 134 | if err == nil { 135 | t.Fatalf("Request should have timed out") 136 | } 137 | 138 | // Build the client with a timeout 139 | client, err = NewClient(client.URL.String() + "?timeout=4") 140 | if err != nil { 141 | t.Fatalf("failed to make client: %s", err) 142 | } 143 | _, err = client.CaptureMessage("Test message") 144 | if err != nil { 145 | t.Fatalf("Request should not have timed out") 146 | } 147 | } 148 | 149 | func decode(buf io.ReadCloser) (ev *Event, err error) { 150 | ev = new(Event) 151 | b64Decoder := base64.NewDecoder(base64.StdEncoding, buf) 152 | reader, err := zlib.NewReader(b64Decoder) 153 | if err != nil { 154 | return 155 | } 156 | 157 | jsonDecoder := json.NewDecoder(reader) 158 | if err = jsonDecoder.Decode(ev); err != nil { 159 | return 160 | } 161 | 162 | if err = reader.Close(); err != nil { 163 | return 164 | } 165 | return ev, nil 166 | } 167 | 168 | func TestStacktrace(t *testing.T) { 169 | var capturedEvent *Event 170 | server := httptest.NewServer(http.HandlerFunc( 171 | func(w http.ResponseWriter, req *http.Request) { 172 | fmt.Fprint(w, "hello") 173 | capturedEvent, _ = decode(req.Body) 174 | })) 175 | defer server.Close() 176 | client := GetClient(server) 177 | 178 | // We nest the calls, and ensur that the correct part of the stack is present 179 | func() { 180 | func() { 181 | client.CaptureMessage("Test With trace") 182 | }() 183 | }() 184 | 185 | // Should be four frames on stack, two for testrunner, two for nesting 186 | if len(capturedEvent.Stacktrace.Frames) != 4 { 187 | t.Fatalf("Wrong number of frames on stack, %v", capturedEvent.Stacktrace) 188 | } 189 | } 190 | --------------------------------------------------------------------------------