├── .dockerignore ├── .gitignore ├── .gitmodules ├── .travis.yml ├── Dockerfile.test ├── LICENSE ├── README.md ├── client.go ├── client_test.go ├── errors.go ├── errors_test.go ├── example └── example.go ├── examples_test.go ├── exception.go ├── exception_test.go ├── http.go ├── http_test.go ├── interfaces.go ├── runtests.sh ├── scripts └── lint.sh ├── stacktrace.go ├── stacktrace_test.go └── writer.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | *.out 3 | example/example 4 | /xunit.xml 5 | /coverage.xml 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/raven-go/5c24d5110e0e198d9ae16f1f3465366085001d92/.gitmodules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.7.x 5 | - 1.8.x 6 | - 1.9.x 7 | - 1.10.x 8 | - 1.11.x 9 | - tip 10 | 11 | before_install: 12 | - go install -race std 13 | - go get golang.org/x/tools/cmd/cover 14 | - go get github.com/tebeka/go2xunit 15 | - go get github.com/t-yuki/gocover-cobertura 16 | - go get -v ./... 17 | 18 | script: 19 | - go test -v -race ./... | tee gotest.out 20 | - $GOPATH/bin/go2xunit -fail -input gotest.out -output xunit.xml 21 | - go test -v -coverprofile=coverage.txt -covermode count . 22 | - $GOPATH/bin/gocover-cobertura < coverage.txt > coverage.xml 23 | 24 | after_script: 25 | - npm install -g @zeus-ci/cli 26 | - zeus upload -t "application/x-cobertura+xml" coverage.xml 27 | - zeus upload -t "application/x-xunit+xml" xunit.xml 28 | 29 | matrix: 30 | include: 31 | - name: "golint 1.9.x" 32 | go: 1.9.x 33 | script: ./scripts/lint.sh 34 | - name: "golint 1.10.x" 35 | go: 1.10.x 36 | script: ./scripts/lint.sh 37 | - name: "golint 1.11.x" 38 | go: 1.11.x 39 | script: ./scripts/lint.sh 40 | allow_failures: 41 | - go: tip 42 | 43 | notifications: 44 | webhooks: 45 | urls: 46 | - https://zeus.ci/hooks/cd949996-d30a-11e8-ba53-0a580a28042d/public/provider/travis/webhook 47 | on_success: always 48 | on_failure: always 49 | on_start: always 50 | on_cancel: always 51 | on_error: always 52 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM golang:1.7 2 | 3 | RUN mkdir -p /go/src/github.com/getsentry/raven-go 4 | WORKDIR /go/src/github.com/getsentry/raven-go 5 | ENV GOPATH /go 6 | 7 | RUN go install -race std && go get golang.org/x/tools/cmd/cover 8 | 9 | COPY . /go/src/github.com/getsentry/raven-go 10 | 11 | RUN go get -v ./... 12 | 13 | CMD ["./runtests.sh"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Apollic Software, LLC. All rights reserved. 2 | Copyright (c) 2015 Functional Software, Inc. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Apollic Software, LLC nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raven 2 | 3 | [![Build Status](https://api.travis-ci.org/getsentry/raven-go.svg?branch=master)](https://travis-ci.org/getsentry/raven-go) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/getsentry/raven-go)](https://goreportcard.com/report/github.com/getsentry/raven-go) 5 | [![GoDoc](https://godoc.org/github.com/getsentry/raven-go?status.svg)](https://godoc.org/github.com/getsentry/raven-go) 6 | 7 | --- 8 | 9 | > The `raven-go` SDK is no longer maintained and was superseded by the `sentry-go` SDK. 10 | > Learn more about the project on [GitHub](https://github.com/getsentry/sentry-go) and check out the [migration guide](https://docs.sentry.io/platforms/go/migration/). 11 | 12 | --- 13 | 14 | raven is the official Go SDK for the [Sentry](https://github.com/getsentry/sentry) 15 | event/error logging system. 16 | 17 | - [**API Documentation**](https://godoc.org/github.com/getsentry/raven-go) 18 | - [**Usage and Examples**](https://docs.sentry.io/clients/go/) 19 | 20 | ## Installation 21 | 22 | ```text 23 | go get github.com/getsentry/raven-go 24 | ``` 25 | 26 | Note: Go 1.7 and newer are supported. 27 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Package raven implements a client for the Sentry error logging service. 2 | package raven 3 | 4 | import ( 5 | "bytes" 6 | "compress/zlib" 7 | "crypto/rand" 8 | "crypto/tls" 9 | "encoding/base64" 10 | "encoding/hex" 11 | "encoding/json" 12 | "errors" 13 | "fmt" 14 | "io" 15 | "io/ioutil" 16 | "log" 17 | mrand "math/rand" 18 | "net/http" 19 | "net/url" 20 | "os" 21 | "regexp" 22 | "runtime" 23 | "strings" 24 | "sync" 25 | "time" 26 | 27 | "github.com/certifi/gocertifi" 28 | ) 29 | 30 | const ( 31 | userAgent = "raven-go/1.0" 32 | timestampFormat = `"2006-01-02T15:04:05.00"` 33 | transportClientTimeout = 30 * time.Second 34 | ) 35 | 36 | // Internal SDK Error types 37 | var ( 38 | ErrPacketDropped = errors.New("raven: packet dropped") 39 | ErrUnableToUnmarshalJSON = errors.New("raven: unable to unmarshal JSON") 40 | ErrMissingUser = errors.New("raven: dsn missing public key and/or password") 41 | ErrMissingProjectID = errors.New("raven: dsn missing project id") 42 | ErrInvalidSampleRate = errors.New("raven: sample rate should be between 0 and 1") 43 | ) 44 | 45 | // Severity used in the level attribute of a message 46 | type Severity string 47 | 48 | // http://docs.python.org/2/howto/logging.html#logging-levels 49 | const ( 50 | DEBUG = Severity("debug") 51 | INFO = Severity("info") 52 | WARNING = Severity("warning") 53 | ERROR = Severity("error") 54 | FATAL = Severity("fatal") 55 | ) 56 | 57 | // Logger used in all internal log calls which can be enabled with SetDebug(true) function call 58 | var debugLogger = log.New(ioutil.Discard, "", 0) 59 | 60 | // Timestamp holds the creation time of a Packet 61 | type Timestamp time.Time 62 | 63 | // MarshalJSON returns the JSON encoding of a timestamp 64 | func (timestamp Timestamp) MarshalJSON() ([]byte, error) { 65 | return []byte(time.Time(timestamp).UTC().Format(timestampFormat)), nil 66 | } 67 | 68 | // UnmarshalJSON sets timestamp to parsed JSON data 69 | func (timestamp *Timestamp) UnmarshalJSON(data []byte) error { 70 | t, err := time.Parse(timestampFormat, string(data)) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | *timestamp = Timestamp(t) 76 | return nil 77 | } 78 | 79 | // Format return timestamp in configured timestampFormat 80 | func (timestamp Timestamp) Format(format string) string { 81 | t := time.Time(timestamp) 82 | return t.Format(format) 83 | } 84 | 85 | // An Interface is a Sentry interface that will be serialized as JSON. 86 | // It must implement json.Marshaler or use json struct tags. 87 | type Interface interface { 88 | // The Sentry class name. Example: sentry.interfaces.Stacktrace 89 | Class() string 90 | } 91 | 92 | // Culpriter holds information about the exception culprit 93 | type Culpriter interface { 94 | Culprit() string 95 | } 96 | 97 | // Transport used in Capture calls that handles communication with the Sentry servers 98 | type Transport interface { 99 | Send(url, authHeader string, packet *Packet) error 100 | } 101 | 102 | // Extra keeps track of any additional information that developer wants to attach to the final packet 103 | type Extra map[string]interface{} 104 | 105 | type outgoingPacket struct { 106 | packet *Packet 107 | ch chan error 108 | } 109 | 110 | // Tag is a key:value pair of strings provided by user to better categorize events 111 | type Tag struct { 112 | Key string 113 | Value string 114 | } 115 | 116 | // Tags keep track of user configured tags 117 | type Tags []Tag 118 | 119 | // MarshalJSON returns the JSON encoding of a tag 120 | func (t *Tag) MarshalJSON() ([]byte, error) { 121 | return json.Marshal([2]string{t.Key, t.Value}) 122 | } 123 | 124 | // UnmarshalJSON sets tag to parsed JSON data 125 | func (t *Tag) UnmarshalJSON(data []byte) error { 126 | var tag [2]string 127 | if err := json.Unmarshal(data, &tag); err != nil { 128 | return err 129 | } 130 | *t = Tag{tag[0], tag[1]} 131 | return nil 132 | } 133 | 134 | // UnmarshalJSON sets tags to parsed JSON data 135 | func (t *Tags) UnmarshalJSON(data []byte) error { 136 | var tags []Tag 137 | 138 | switch data[0] { 139 | case '[': 140 | // Unmarshal into []Tag 141 | if err := json.Unmarshal(data, &tags); err != nil { 142 | return err 143 | } 144 | case '{': 145 | // Unmarshal into map[string]string 146 | tagMap := make(map[string]string) 147 | if err := json.Unmarshal(data, &tagMap); err != nil { 148 | return err 149 | } 150 | 151 | // Convert to []Tag 152 | for k, v := range tagMap { 153 | tags = append(tags, Tag{k, v}) 154 | } 155 | default: 156 | return ErrUnableToUnmarshalJSON 157 | } 158 | 159 | *t = tags 160 | return nil 161 | } 162 | 163 | // Packet defines Sentry's spec compliant interface holding Event information (top-level object) - https://docs.sentry.io/development/sdk-dev/attributes/ 164 | type Packet struct { 165 | // Required 166 | Message string `json:"message"` 167 | 168 | // Required, set automatically by Client.Send/Report via Packet.Init if blank 169 | EventID string `json:"event_id"` 170 | Project string `json:"project"` 171 | Timestamp Timestamp `json:"timestamp"` 172 | Level Severity `json:"level"` 173 | Logger string `json:"logger"` 174 | 175 | // Optional 176 | Platform string `json:"platform,omitempty"` 177 | Culprit string `json:"culprit,omitempty"` 178 | ServerName string `json:"server_name,omitempty"` 179 | Release string `json:"release,omitempty"` 180 | Environment string `json:"environment,omitempty"` 181 | Tags Tags `json:"tags,omitempty"` 182 | Modules map[string]string `json:"modules,omitempty"` 183 | Fingerprint []string `json:"fingerprint,omitempty"` 184 | Extra Extra `json:"extra,omitempty"` 185 | 186 | Interfaces []Interface `json:"-"` 187 | } 188 | 189 | // NewPacket constructs a packet with the specified message and interfaces. 190 | func NewPacket(message string, interfaces ...Interface) *Packet { 191 | extra := Extra{} 192 | setExtraDefaults(extra) 193 | return &Packet{ 194 | Message: message, 195 | Interfaces: interfaces, 196 | Extra: extra, 197 | } 198 | } 199 | 200 | // NewPacketWithExtra constructs a packet with the specified message, extra information, and interfaces. 201 | func NewPacketWithExtra(message string, extra Extra, interfaces ...Interface) *Packet { 202 | if extra == nil { 203 | extra = Extra{} 204 | } 205 | setExtraDefaults(extra) 206 | 207 | return &Packet{ 208 | Message: message, 209 | Interfaces: interfaces, 210 | Extra: extra, 211 | } 212 | } 213 | 214 | func setExtraDefaults(extra Extra) Extra { 215 | extra["runtime.Version"] = runtime.Version() 216 | extra["runtime.NumCPU"] = runtime.NumCPU() 217 | extra["runtime.GOMAXPROCS"] = runtime.GOMAXPROCS(0) // 0 just returns the current value 218 | extra["runtime.NumGoroutine"] = runtime.NumGoroutine() 219 | return extra 220 | } 221 | 222 | // Init initializes required fields in a packet. It is typically called by 223 | // Client.Send/Report automatically. 224 | func (packet *Packet) Init(project string) error { 225 | if packet.Project == "" { 226 | packet.Project = project 227 | } 228 | if packet.EventID == "" { 229 | var err error 230 | packet.EventID, err = uuid() 231 | if err != nil { 232 | return err 233 | } 234 | } 235 | if time.Time(packet.Timestamp).IsZero() { 236 | packet.Timestamp = Timestamp(time.Now()) 237 | } 238 | if packet.Level == "" { 239 | packet.Level = ERROR 240 | } 241 | if packet.Logger == "" { 242 | packet.Logger = "root" 243 | } 244 | if packet.ServerName == "" { 245 | packet.ServerName = hostname 246 | } 247 | if packet.Platform == "" { 248 | packet.Platform = "go" 249 | } 250 | 251 | if packet.Culprit == "" { 252 | for _, inter := range packet.Interfaces { 253 | if c, ok := inter.(Culpriter); ok { 254 | packet.Culprit = c.Culprit() 255 | if packet.Culprit != "" { 256 | break 257 | } 258 | } 259 | } 260 | } 261 | 262 | return nil 263 | } 264 | 265 | // AddTags appends new tags to the existing ones 266 | func (packet *Packet) AddTags(tags map[string]string) { 267 | for k, v := range tags { 268 | packet.Tags = append(packet.Tags, Tag{k, v}) 269 | } 270 | } 271 | 272 | func uuid() (string, error) { 273 | id := make([]byte, 16) 274 | _, err := io.ReadFull(rand.Reader, id) 275 | if err != nil { 276 | return "", err 277 | } 278 | id[6] &= 0x0F // clear version 279 | id[6] |= 0x40 // set version to 4 (random uuid) 280 | id[8] &= 0x3F // clear variant 281 | id[8] |= 0x80 // set to IETF variant 282 | return hex.EncodeToString(id), nil 283 | } 284 | 285 | // JSON encodes packet into JSON format that will be sent to the server 286 | func (packet *Packet) JSON() ([]byte, error) { 287 | packetJSON, err := json.Marshal(packet) 288 | if err != nil { 289 | return nil, err 290 | } 291 | 292 | interfaces := make(map[string]Interface, len(packet.Interfaces)) 293 | for _, inter := range packet.Interfaces { 294 | if inter != nil { 295 | interfaces[inter.Class()] = inter 296 | } 297 | } 298 | 299 | if len(interfaces) > 0 { 300 | interfaceJSON, err := json.Marshal(interfaces) 301 | if err != nil { 302 | return nil, err 303 | } 304 | packetJSON[len(packetJSON)-1] = ',' 305 | packetJSON = append(packetJSON, interfaceJSON[1:]...) 306 | } 307 | 308 | return packetJSON, nil 309 | } 310 | 311 | type context struct { 312 | user *User 313 | http *Http 314 | tags map[string]string 315 | } 316 | 317 | func (c *context) setUser(u *User) { c.user = u } 318 | func (c *context) setHttp(h *Http) { c.http = h } 319 | func (c *context) setTags(t map[string]string) { 320 | if c.tags == nil { 321 | c.tags = make(map[string]string) 322 | } 323 | for k, v := range t { 324 | c.tags[k] = v 325 | } 326 | } 327 | func (c *context) clear() { 328 | c.user = nil 329 | c.http = nil 330 | c.tags = nil 331 | } 332 | 333 | // Return a list of interfaces to be used in appending with the rest 334 | func (c *context) interfaces() []Interface { 335 | len, i := 0, 0 336 | if c.user != nil { 337 | len++ 338 | } 339 | if c.http != nil { 340 | len++ 341 | } 342 | interfaces := make([]Interface, len) 343 | if c.user != nil { 344 | interfaces[i] = c.user 345 | i++ 346 | } 347 | if c.http != nil { 348 | interfaces[i] = c.http 349 | } 350 | return interfaces 351 | } 352 | 353 | // MaxQueueBuffer the maximum number of packets that will be buffered waiting to be delivered. 354 | // Packets will be dropped if the buffer is full. Used by NewClient. 355 | var MaxQueueBuffer = 100 356 | 357 | func SetMaxQueueBuffer(maxCount int) { 358 | MaxQueueBuffer = maxCount 359 | } 360 | 361 | func newTransport() Transport { 362 | t := &HTTPTransport{} 363 | rootCAs, err := gocertifi.CACerts() 364 | if err != nil { 365 | debugLogger.Println("failed to load root TLS certificates:", err) 366 | } else { 367 | t.Client = &http.Client{ 368 | Transport: &http.Transport{ 369 | Proxy: http.ProxyFromEnvironment, 370 | TLSClientConfig: &tls.Config{RootCAs: rootCAs}, 371 | }, 372 | Timeout: transportClientTimeout, 373 | } 374 | } 375 | return t 376 | } 377 | 378 | func newClient(tags map[string]string) *Client { 379 | client := &Client{ 380 | Transport: newTransport(), 381 | Tags: tags, 382 | context: &context{}, 383 | sampleRate: 1.0, 384 | queue: make(chan *outgoingPacket, MaxQueueBuffer), 385 | } 386 | err := client.SetDSN(os.Getenv("SENTRY_DSN")) 387 | 388 | if err != nil { 389 | debugLogger.Println("incorrect DSN", err) 390 | } 391 | 392 | client.SetRelease(os.Getenv("SENTRY_RELEASE")) 393 | client.SetEnvironment(os.Getenv("SENTRY_ENVIRONMENT")) 394 | return client 395 | } 396 | 397 | // New constructs a new Sentry client instance 398 | func New(dsn string) (*Client, error) { 399 | client := newClient(nil) 400 | return client, client.SetDSN(dsn) 401 | } 402 | 403 | // NewWithTags constructs a new Sentry client instance with default tags. 404 | func NewWithTags(dsn string, tags map[string]string) (*Client, error) { 405 | client := newClient(tags) 406 | return client, client.SetDSN(dsn) 407 | } 408 | 409 | // NewClient constructs a Sentry client and spawns a background goroutine to 410 | // handle packets sent by Client.Report. 411 | // 412 | // Deprecated: use New and NewWithTags instead 413 | func NewClient(dsn string, tags map[string]string) (*Client, error) { 414 | client := newClient(tags) 415 | return client, client.SetDSN(dsn) 416 | } 417 | 418 | // Client encapsulates a connection to a Sentry server. It must be initialized 419 | // by calling NewClient. Modification of fields concurrently with Send or after 420 | // calling Report for the first time is not thread-safe. 421 | type Client struct { 422 | Tags map[string]string 423 | 424 | Transport Transport 425 | 426 | // DropHandler is called when a packet is dropped because the buffer is full. 427 | DropHandler func(*Packet) 428 | 429 | // Context that will get appending to all packets 430 | context *context 431 | 432 | mu sync.RWMutex 433 | url string 434 | projectID string 435 | authHeader string 436 | release string 437 | environment string 438 | sampleRate float32 439 | 440 | // default logger name (leave empty for 'root') 441 | defaultLoggerName string 442 | 443 | includePaths []string 444 | ignoreErrorsRegexp *regexp.Regexp 445 | queue chan *outgoingPacket 446 | 447 | // A WaitGroup to keep track of all currently in-progress captures 448 | // This is intended to be used with Client.Wait() to assure that 449 | // all messages have been transported before exiting the process. 450 | wg sync.WaitGroup 451 | 452 | // A Once to track only starting up the background worker once 453 | start sync.Once 454 | } 455 | 456 | // DefaultClient initialize a default *Client instance 457 | var DefaultClient = newClient(nil) 458 | 459 | // SetIgnoreErrors updates ignoreErrors config on given client 460 | func (client *Client) SetIgnoreErrors(errs []string) error { 461 | joinedRegexp := strings.Join(errs, "|") 462 | r, err := regexp.Compile(joinedRegexp) 463 | if err != nil { 464 | return fmt.Errorf("raven: failed to compile regexp %q for %q: %v", joinedRegexp, errs, err) 465 | } 466 | 467 | client.mu.Lock() 468 | client.ignoreErrorsRegexp = r 469 | client.mu.Unlock() 470 | return nil 471 | } 472 | 473 | func (client *Client) shouldExcludeErr(errStr string) bool { 474 | client.mu.RLock() 475 | defer client.mu.RUnlock() 476 | return client.ignoreErrorsRegexp != nil && client.ignoreErrorsRegexp.MatchString(errStr) 477 | } 478 | 479 | // SetIgnoreErrors updates ignoreErrors config on default client 480 | func SetIgnoreErrors(errs ...string) error { 481 | return DefaultClient.SetIgnoreErrors(errs) 482 | } 483 | 484 | // SetDSN updates a client with a new DSN. It safe to call after and 485 | // concurrently with calls to Report and Send. 486 | func (client *Client) SetDSN(dsn string) error { 487 | if dsn == "" { 488 | return nil 489 | } 490 | 491 | client.mu.Lock() 492 | defer client.mu.Unlock() 493 | 494 | uri, err := url.Parse(dsn) 495 | if err != nil { 496 | return err 497 | } 498 | 499 | if uri.User == nil { 500 | return ErrMissingUser 501 | } 502 | publicKey := uri.User.Username() 503 | secretKey, hasSecretKey := uri.User.Password() 504 | uri.User = nil 505 | 506 | if idx := strings.LastIndex(uri.Path, "/"); idx != -1 { 507 | client.projectID = uri.Path[idx+1:] 508 | uri.Path = uri.Path[:idx+1] + "api/" + client.projectID + "/store/" 509 | } 510 | if client.projectID == "" { 511 | return ErrMissingProjectID 512 | } 513 | 514 | client.url = uri.String() 515 | 516 | if hasSecretKey { 517 | client.authHeader = fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s, sentry_secret=%s", publicKey, secretKey) 518 | } else { 519 | client.authHeader = fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s", publicKey) 520 | } 521 | 522 | return nil 523 | } 524 | 525 | // SetDSN sets the DSN for the default *Client instance 526 | func SetDSN(dsn string) error { return DefaultClient.SetDSN(dsn) } 527 | 528 | // SetRelease sets the "release" tag. 529 | func (client *Client) SetRelease(release string) { 530 | client.mu.Lock() 531 | defer client.mu.Unlock() 532 | client.release = release 533 | } 534 | 535 | // SetEnvironment sets the "environment" tag. 536 | func (client *Client) SetEnvironment(environment string) { 537 | client.mu.Lock() 538 | defer client.mu.Unlock() 539 | client.environment = environment 540 | } 541 | 542 | // SetDefaultLoggerName sets the default logger name. 543 | func (client *Client) SetDefaultLoggerName(name string) { 544 | client.mu.Lock() 545 | defer client.mu.Unlock() 546 | client.defaultLoggerName = name 547 | } 548 | 549 | // SetSampleRate sets how much sampling we want on client side 550 | func (client *Client) SetSampleRate(rate float32) error { 551 | client.mu.Lock() 552 | defer client.mu.Unlock() 553 | 554 | if rate < 0 || rate > 1 { 555 | return ErrInvalidSampleRate 556 | } 557 | client.sampleRate = rate 558 | return nil 559 | } 560 | 561 | func (client *Client) SetDebug(debug bool) { 562 | if debug == true { 563 | debugLogger = log.New(os.Stdout, "raven: ", 0) 564 | } else { 565 | debugLogger = log.New(ioutil.Discard, "", 0) 566 | } 567 | } 568 | 569 | // SetRelease sets the "release" tag on the default *Client 570 | func SetRelease(release string) { DefaultClient.SetRelease(release) } 571 | 572 | // SetEnvironment sets the "environment" tag on the default *Client 573 | func SetEnvironment(environment string) { DefaultClient.SetEnvironment(environment) } 574 | 575 | // SetDefaultLoggerName sets the "defaultLoggerName" on the default *Client 576 | func SetDefaultLoggerName(name string) { 577 | DefaultClient.SetDefaultLoggerName(name) 578 | } 579 | 580 | // SetSampleRate sets the "sample rate" on the degault *Client 581 | func SetSampleRate(rate float32) error { return DefaultClient.SetSampleRate(rate) } 582 | 583 | // SetDebug sets the "debug" config on the default *Client 584 | func SetDebug(debug bool) { DefaultClient.SetDebug(debug) } 585 | 586 | func (client *Client) worker() { 587 | for outgoingPacket := range client.queue { 588 | 589 | client.mu.RLock() 590 | url, authHeader := client.url, client.authHeader 591 | client.mu.RUnlock() 592 | 593 | outgoingPacket.ch <- client.Transport.Send(url, authHeader, outgoingPacket.packet) 594 | client.wg.Done() 595 | } 596 | } 597 | 598 | // Capture asynchronously delivers a packet to the Sentry server. It is a no-op 599 | // when client is nil. A channel is provided if it is important to check for a 600 | // send's success. 601 | func (client *Client) Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) { 602 | ch = make(chan error, 1) 603 | 604 | if client == nil { 605 | // return a chan that always returns nil when the caller receives from it 606 | close(ch) 607 | return 608 | } 609 | 610 | if client.sampleRate < 1.0 && mrand.Float32() > client.sampleRate { 611 | return 612 | } 613 | 614 | if packet == nil { 615 | close(ch) 616 | return 617 | } 618 | 619 | if client.shouldExcludeErr(packet.Message) { 620 | return 621 | } 622 | 623 | // Keep track of all running Captures so that we can wait for them all to finish 624 | // *Must* call client.wg.Done() on any path that indicates that an event was 625 | // finished being acted upon, whether success or failure 626 | client.wg.Add(1) 627 | 628 | // Merge capture tags and client tags 629 | packet.AddTags(captureTags) 630 | packet.AddTags(client.Tags) 631 | 632 | // Initialize any required packet fields 633 | client.mu.RLock() 634 | packet.AddTags(client.context.tags) 635 | projectID := client.projectID 636 | release := client.release 637 | environment := client.environment 638 | defaultLoggerName := client.defaultLoggerName 639 | client.mu.RUnlock() 640 | 641 | // set the global logger name on the packet if we must 642 | if packet.Logger == "" && defaultLoggerName != "" { 643 | packet.Logger = defaultLoggerName 644 | } 645 | 646 | // Set Severity if value is provided 647 | if Severity(captureTags["level"]) != "" { 648 | packet.Level = Severity(captureTags["level"]) 649 | } 650 | 651 | err := packet.Init(projectID) 652 | if err != nil { 653 | ch <- err 654 | client.wg.Done() 655 | return 656 | } 657 | 658 | if packet.Release == "" { 659 | packet.Release = release 660 | } 661 | 662 | if packet.Environment == "" { 663 | packet.Environment = environment 664 | } 665 | 666 | outgoingPacket := &outgoingPacket{packet, ch} 667 | 668 | // Lazily start background worker until we 669 | // do our first write into the queue. 670 | client.start.Do(func() { 671 | go client.worker() 672 | }) 673 | 674 | select { 675 | case client.queue <- outgoingPacket: 676 | default: 677 | // Send would block, drop the packet 678 | if client.DropHandler != nil { 679 | client.DropHandler(packet) 680 | } 681 | ch <- ErrPacketDropped 682 | client.wg.Done() 683 | } 684 | 685 | return packet.EventID, ch 686 | } 687 | 688 | // Capture asynchronously delivers a packet to the Sentry server with the default *Client. 689 | // It is a no-op when client is nil. A channel is provided if it is important to check for a 690 | // send's success. 691 | func Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) { 692 | return DefaultClient.Capture(packet, captureTags) 693 | } 694 | 695 | // CaptureMessage formats and delivers a string message to the Sentry server. 696 | func (client *Client) CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string { 697 | if client == nil { 698 | return "" 699 | } 700 | 701 | if client.shouldExcludeErr(message) { 702 | return "" 703 | } 704 | 705 | packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...) 706 | eventID, _ := client.Capture(packet, tags) 707 | 708 | return eventID 709 | } 710 | 711 | // CaptureMessage formats and delivers a string message to the Sentry server with the default *Client 712 | func CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string { 713 | return DefaultClient.CaptureMessage(message, tags, interfaces...) 714 | } 715 | 716 | // CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent. 717 | func (client *Client) CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string { 718 | if client == nil { 719 | return "" 720 | } 721 | 722 | if client.shouldExcludeErr(message) { 723 | return "" 724 | } 725 | 726 | packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...) 727 | eventID, ch := client.Capture(packet, tags) 728 | if eventID != "" { 729 | <-ch 730 | } 731 | 732 | return eventID 733 | } 734 | 735 | // CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent. 736 | func CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string { 737 | return DefaultClient.CaptureMessageAndWait(message, tags, interfaces...) 738 | } 739 | 740 | // CaptureError formats and delivers an error to the Sentry server. 741 | // Adds a stacktrace to the packet, excluding the call to this method. 742 | func (client *Client) CaptureError(err error, tags map[string]string, interfaces ...Interface) string { 743 | if client == nil { 744 | return "" 745 | } 746 | 747 | if err == nil { 748 | return "" 749 | } 750 | 751 | if client.shouldExcludeErr(err.Error()) { 752 | return "" 753 | } 754 | 755 | extra := extractExtra(err) 756 | cause := Cause(err) 757 | 758 | packet := NewPacketWithExtra(err.Error(), extra, append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...) 759 | eventID, _ := client.Capture(packet, tags) 760 | 761 | return eventID 762 | } 763 | 764 | // CaptureError formats and delivers an error to the Sentry server using the default *Client. 765 | // Adds a stacktrace to the packet, excluding the call to this method. 766 | func CaptureError(err error, tags map[string]string, interfaces ...Interface) string { 767 | return DefaultClient.CaptureError(err, tags, interfaces...) 768 | } 769 | 770 | // CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent 771 | func (client *Client) CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string { 772 | if client == nil { 773 | return "" 774 | } 775 | 776 | if client.shouldExcludeErr(err.Error()) { 777 | return "" 778 | } 779 | 780 | extra := extractExtra(err) 781 | cause := Cause(err) 782 | 783 | packet := NewPacketWithExtra(err.Error(), extra, append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...) 784 | eventID, ch := client.Capture(packet, tags) 785 | if eventID != "" { 786 | <-ch 787 | } 788 | 789 | return eventID 790 | } 791 | 792 | // CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent 793 | func CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string { 794 | return DefaultClient.CaptureErrorAndWait(err, tags, interfaces...) 795 | } 796 | 797 | // CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs. 798 | // If an error is captured, both the error and the reported Sentry error ID are returned. 799 | func (client *Client) CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) { 800 | // Note: This doesn't need to check for client, because we still want to go through the defer/recover path 801 | // Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the 802 | // *Packet just to be thrown away, this should not be the normal case. Could be refactored to 803 | // be completely noop though if we cared. 804 | defer func() { 805 | var packet *Packet 806 | err = recover() 807 | switch rval := err.(type) { 808 | case nil: 809 | return 810 | case error: 811 | if client.shouldExcludeErr(rval.Error()) { 812 | return 813 | } 814 | packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...) 815 | default: 816 | rvalStr := fmt.Sprint(rval) 817 | if client.shouldExcludeErr(rvalStr) { 818 | return 819 | } 820 | packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...) 821 | } 822 | 823 | errorID, _ = client.Capture(packet, tags) 824 | }() 825 | 826 | f() 827 | return 828 | } 829 | 830 | // CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs. 831 | // If an error is captured, both the error and the reported Sentry error ID are returned. 832 | func CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) { 833 | return DefaultClient.CapturePanic(f, tags, interfaces...) 834 | } 835 | 836 | // CapturePanicAndWait is identical to CapturePanic, except it blocks and assures that the event was sent 837 | func (client *Client) CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) { 838 | // Note: This doesn't need to check for client, because we still want to go through the defer/recover path 839 | // Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the 840 | // *Packet just to be thrown away, this should not be the normal case. Could be refactored to 841 | // be completely noop though if we cared. 842 | defer func() { 843 | var packet *Packet 844 | err = recover() 845 | switch rval := err.(type) { 846 | case nil: 847 | return 848 | case error: 849 | if client.shouldExcludeErr(rval.Error()) { 850 | return 851 | } 852 | packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...) 853 | default: 854 | rvalStr := fmt.Sprint(rval) 855 | if client.shouldExcludeErr(rvalStr) { 856 | return 857 | } 858 | packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...) 859 | } 860 | 861 | var ch chan error 862 | errorID, ch = client.Capture(packet, tags) 863 | if errorID != "" { 864 | <-ch 865 | } 866 | }() 867 | 868 | f() 869 | return 870 | } 871 | 872 | // CapturePanicAndWait is identical to CapturePanic, except it blocks and assures that the event was sent 873 | func CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) { 874 | return DefaultClient.CapturePanicAndWait(f, tags, interfaces...) 875 | } 876 | 877 | // Close given clients event queue 878 | func (client *Client) Close() { 879 | close(client.queue) 880 | } 881 | 882 | // Close defaults client event queue 883 | func Close() { DefaultClient.Close() } 884 | 885 | // Wait blocks and waits for all events to finish being sent to Sentry server 886 | func (client *Client) Wait() { 887 | client.wg.Wait() 888 | } 889 | 890 | // Wait blocks and waits for all events to finish being sent to Sentry server 891 | func Wait() { DefaultClient.Wait() } 892 | 893 | // URL returns configured url of given client 894 | func (client *Client) URL() string { 895 | client.mu.RLock() 896 | defer client.mu.RUnlock() 897 | 898 | return client.url 899 | } 900 | 901 | // URL returns configured url of default client 902 | func URL() string { return DefaultClient.URL() } 903 | 904 | // ProjectID returns configured ProjectID of given client 905 | func (client *Client) ProjectID() string { 906 | client.mu.RLock() 907 | defer client.mu.RUnlock() 908 | 909 | return client.projectID 910 | } 911 | 912 | // ProjectID returns configured ProjectID of default client 913 | func ProjectID() string { return DefaultClient.ProjectID() } 914 | 915 | // Release returns configured Release of given client 916 | func (client *Client) Release() string { 917 | client.mu.RLock() 918 | defer client.mu.RUnlock() 919 | 920 | return client.release 921 | } 922 | 923 | // Release returns configured Release of default client 924 | func Release() string { return DefaultClient.Release() } 925 | 926 | // IncludePaths returns configured includePaths of given client 927 | func (client *Client) IncludePaths() []string { 928 | client.mu.RLock() 929 | defer client.mu.RUnlock() 930 | 931 | return client.includePaths 932 | } 933 | 934 | // IncludePaths returns configured includePaths of default client 935 | func IncludePaths() []string { return DefaultClient.IncludePaths() } 936 | 937 | // SetIncludePaths updates includePaths config on given client 938 | func (client *Client) SetIncludePaths(p []string) { 939 | client.mu.Lock() 940 | defer client.mu.Unlock() 941 | 942 | client.includePaths = p 943 | } 944 | 945 | // SetIncludePaths updates includePaths config on default client 946 | func SetIncludePaths(p []string) { DefaultClient.SetIncludePaths(p) } 947 | 948 | // SetUserContext updates User of Context interface on given client 949 | func (client *Client) SetUserContext(u *User) { 950 | client.mu.Lock() 951 | defer client.mu.Unlock() 952 | client.context.setUser(u) 953 | } 954 | 955 | // SetHttpContext updates Http of Context interface on given client 956 | func (client *Client) SetHttpContext(h *Http) { 957 | client.mu.Lock() 958 | defer client.mu.Unlock() 959 | client.context.setHttp(h) 960 | } 961 | 962 | // SetTagsContext updates Tags of Context interface on given client 963 | func (client *Client) SetTagsContext(t map[string]string) { 964 | client.mu.Lock() 965 | defer client.mu.Unlock() 966 | client.context.setTags(t) 967 | } 968 | 969 | // ClearContext clears Context interface on given client by removing tags, user and request information 970 | func (client *Client) ClearContext() { 971 | client.mu.Lock() 972 | defer client.mu.Unlock() 973 | client.context.clear() 974 | } 975 | 976 | // SetUserContext updates User of Context interface on default client 977 | func SetUserContext(u *User) { DefaultClient.SetUserContext(u) } 978 | 979 | // SetHttpContext updates Http of Context interface on default client 980 | func SetHttpContext(h *Http) { DefaultClient.SetHttpContext(h) } 981 | 982 | // SetTagsContext updates Tags of Context interface on default client 983 | func SetTagsContext(t map[string]string) { DefaultClient.SetTagsContext(t) } 984 | 985 | // ClearContext clears Context interface on default client by removing tags, user and request information 986 | func ClearContext() { DefaultClient.ClearContext() } 987 | 988 | // HTTPTransport is the default transport, delivering packets to Sentry via the 989 | // HTTP API. 990 | type HTTPTransport struct { 991 | *http.Client 992 | } 993 | 994 | // Send uses HTTPTransport to send a Packet to configured Sentry's DSN endpoint 995 | func (t *HTTPTransport) Send(url, authHeader string, packet *Packet) error { 996 | if url == "" { 997 | return nil 998 | } 999 | 1000 | body, contentType, err := serializedPacket(packet) 1001 | if err != nil { 1002 | return fmt.Errorf("raven: error serializing packet: %v", err) 1003 | } 1004 | req, err := http.NewRequest("POST", url, body) 1005 | if err != nil { 1006 | return fmt.Errorf("raven: can't create new request: %v", err) 1007 | } 1008 | req.Header.Set("X-Sentry-Auth", authHeader) 1009 | req.Header.Set("User-Agent", userAgent) 1010 | req.Header.Set("Content-Type", contentType) 1011 | 1012 | res, err := t.Do(req) 1013 | if err != nil { 1014 | return err 1015 | } 1016 | 1017 | // Response body needs to be drained and closed in order for TCP connection to stay opened (via keep-alive) and reused 1018 | _, err = io.Copy(ioutil.Discard, res.Body) 1019 | if err != nil { 1020 | debugLogger.Println("Error while reading response body", res) 1021 | } 1022 | 1023 | err = res.Body.Close() 1024 | if err != nil { 1025 | debugLogger.Println("Error while closing response body", err) 1026 | } 1027 | 1028 | if res.StatusCode != 200 { 1029 | return fmt.Errorf("raven: got http status %d - x-sentry-error: %s", res.StatusCode, res.Header.Get("X-Sentry-Error")) 1030 | } 1031 | return nil 1032 | } 1033 | 1034 | func serializedPacket(packet *Packet) (io.Reader, string, error) { 1035 | packetJSON, err := packet.JSON() 1036 | if err != nil { 1037 | return nil, "", fmt.Errorf("raven: error marshaling packet %+v to JSON: %v", packet, err) 1038 | } 1039 | 1040 | // Only deflate/base64 the packet if it is bigger than 1KB, as there is an overhead 1041 | if len(packetJSON) > 1000 { 1042 | buf := &bytes.Buffer{} 1043 | b64 := base64.NewEncoder(base64.StdEncoding, buf) 1044 | deflate, _ := zlib.NewWriterLevel(b64, zlib.BestCompression) 1045 | _, err := deflate.Write(packetJSON) 1046 | if err != nil { 1047 | debugLogger.Println("Error while deflating data in packet serializer", err) 1048 | } 1049 | err = deflate.Close() 1050 | if err != nil { 1051 | debugLogger.Println("Error while closing zlib deflate in packet serializer", err) 1052 | } 1053 | err = b64.Close() 1054 | if err != nil { 1055 | debugLogger.Println("Error while closing b64 encoder in packet serializer", err) 1056 | } 1057 | return buf, "application/octet-stream", nil 1058 | } 1059 | return bytes.NewReader(packetJSON), "application/json", nil 1060 | } 1061 | 1062 | var hostname string 1063 | 1064 | func init() { 1065 | hostname, _ = os.Hostname() 1066 | } 1067 | 1068 | // Cause returns the underlying cause of the error, if possible. 1069 | // An error value has a cause if it implements the following 1070 | // interface: 1071 | // 1072 | // type causer interface { 1073 | // Cause() error 1074 | // } 1075 | // 1076 | // If the error does not implement Cause, the original error will 1077 | // be returned. 1078 | // 1079 | // If the cause of the error is nil, then the original 1080 | // error will be returned. 1081 | // 1082 | // If the error is nil, nil will be returned without further 1083 | // investigation. 1084 | // 1085 | // Will return the deepest cause which is not nil. 1086 | func Cause(err error) error { 1087 | type causer interface { 1088 | Cause() error 1089 | } 1090 | 1091 | for err != nil { 1092 | cause, ok := err.(causer) 1093 | if !ok { 1094 | break 1095 | } 1096 | 1097 | if _cause := cause.Cause(); _cause != nil { 1098 | err = _cause 1099 | } else { 1100 | break 1101 | } 1102 | 1103 | } 1104 | return err 1105 | } 1106 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | pkgErrors "github.com/pkg/errors" 7 | "reflect" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | type testInterface struct{} 13 | 14 | func (t *testInterface) Class() string { return "sentry.interfaces.Test" } 15 | func (t *testInterface) Culprit() string { return "codez" } 16 | 17 | func TestShouldExcludeErr(t *testing.T) { 18 | regexpStrs := []string{"ERR_TIMEOUT", "should.exclude", "(?i)^big$"} 19 | 20 | client := &Client{ 21 | Transport: newTransport(), 22 | Tags: nil, 23 | context: &context{}, 24 | queue: make(chan *outgoingPacket, MaxQueueBuffer), 25 | } 26 | 27 | if err := client.SetIgnoreErrors(regexpStrs); err != nil { 28 | t.Fatalf("invalid regexps %v: %v", regexpStrs, err) 29 | } 30 | 31 | testCases := []string{ 32 | "there was a ERR_TIMEOUT in handlers.go", 33 | "do not log should.exclude at all", 34 | "BIG", 35 | } 36 | 37 | for _, tc := range testCases { 38 | if !client.shouldExcludeErr(tc) { 39 | t.Fatalf("failed to exclude err %q with regexps %v", tc, regexpStrs) 40 | } 41 | } 42 | } 43 | 44 | func TestPacketJSON(t *testing.T) { 45 | packet := &Packet{ 46 | Project: "1", 47 | EventID: "2", 48 | Platform: "linux", 49 | Culprit: "caused_by", 50 | ServerName: "host1", 51 | Release: "721e41770371db95eee98ca2707686226b993eda", 52 | Environment: "production", 53 | Message: "test", 54 | Timestamp: Timestamp(time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC)), 55 | Level: ERROR, 56 | Logger: "com.getsentry.raven-go.logger-test-packet-json", 57 | Tags: []Tag{{"foo", "bar"}}, 58 | Modules: map[string]string{"foo": "bar"}, 59 | Fingerprint: []string{"{{ default }}", "a-custom-fingerprint"}, 60 | Interfaces: []Interface{&Message{Message: "foo"}}, 61 | } 62 | 63 | packet.AddTags(map[string]string{"foo": "foo"}) 64 | packet.AddTags(map[string]string{"baz": "buzz"}) 65 | 66 | expected := `{"message":"test","event_id":"2","project":"1","timestamp":"2000-01-01T00:00:00.00","level":"error","logger":"com.getsentry.raven-go.logger-test-packet-json","platform":"linux","culprit":"caused_by","server_name":"host1","release":"721e41770371db95eee98ca2707686226b993eda","environment":"production","tags":[["foo","bar"],["foo","foo"],["baz","buzz"]],"modules":{"foo":"bar"},"fingerprint":["{{ default }}","a-custom-fingerprint"],"logentry":{"message":"foo"}}` 67 | j, err := packet.JSON() 68 | if err != nil { 69 | t.Fatalf("JSON marshalling should not fail: %v", err) 70 | } 71 | actual := string(j) 72 | 73 | if actual != expected { 74 | t.Errorf("incorrect json; got %s, want %s", actual, expected) 75 | } 76 | } 77 | 78 | func TestPacketJSONNilInterface(t *testing.T) { 79 | packet := &Packet{ 80 | Project: "1", 81 | EventID: "2", 82 | Platform: "linux", 83 | Culprit: "caused_by", 84 | ServerName: "host1", 85 | Release: "721e41770371db95eee98ca2707686226b993eda", 86 | Environment: "production", 87 | Message: "test", 88 | Timestamp: Timestamp(time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC)), 89 | Level: ERROR, 90 | Logger: "com.getsentry.raven-go.logger-test-packet-json", 91 | Tags: []Tag{{"foo", "bar"}}, 92 | Modules: map[string]string{"foo": "bar"}, 93 | Fingerprint: []string{"{{ default }}", "a-custom-fingerprint"}, 94 | Interfaces: []Interface{&Message{Message: "foo"}, nil}, 95 | } 96 | 97 | expected := `{"message":"test","event_id":"2","project":"1","timestamp":"2000-01-01T00:00:00.00","level":"error","logger":"com.getsentry.raven-go.logger-test-packet-json","platform":"linux","culprit":"caused_by","server_name":"host1","release":"721e41770371db95eee98ca2707686226b993eda","environment":"production","tags":[["foo","bar"]],"modules":{"foo":"bar"},"fingerprint":["{{ default }}","a-custom-fingerprint"],"logentry":{"message":"foo"}}` 98 | j, err := packet.JSON() 99 | if err != nil { 100 | t.Fatalf("JSON marshalling should not fail: %v", err) 101 | } 102 | actual := string(j) 103 | 104 | if actual != expected { 105 | t.Errorf("incorrect json; got %s, want %s", actual, expected) 106 | } 107 | } 108 | 109 | func TestPacketInit(t *testing.T) { 110 | packet := &Packet{Message: "a", Interfaces: []Interface{&testInterface{}}} 111 | err := packet.Init("foo") 112 | 113 | if err != nil { 114 | fmt.Println("failed to initialize packet") 115 | } 116 | 117 | if packet.Project != "foo" { 118 | t.Error("incorrect Project:", packet.Project) 119 | } 120 | if packet.Culprit != "codez" { 121 | t.Error("incorrect Culprit:", packet.Culprit) 122 | } 123 | if packet.ServerName == "" { 124 | t.Errorf("ServerName should not be empty") 125 | } 126 | if packet.Level != ERROR { 127 | t.Errorf("incorrect Level: got %s, want %s", packet.Level, ERROR) 128 | } 129 | if packet.Logger != "root" { 130 | t.Errorf("incorrect Logger: got %s, want %s", packet.Logger, "root") 131 | } 132 | if time.Time(packet.Timestamp).IsZero() { 133 | t.Error("Timestamp is zero") 134 | } 135 | if len(packet.EventID) != 32 { 136 | t.Error("incorrect EventID:", packet.EventID) 137 | } 138 | } 139 | 140 | func TestSetDSN(t *testing.T) { 141 | client := &Client{} 142 | err := client.SetDSN("https://u:p@example.com/sentry/1") 143 | 144 | if err != nil { 145 | fmt.Println("invalid DSN") 146 | } 147 | 148 | if client.url != "https://example.com/sentry/api/1/store/" { 149 | t.Error("incorrect url:", client.url) 150 | } 151 | if client.projectID != "1" { 152 | t.Error("incorrect projectID:", client.projectID) 153 | } 154 | if client.authHeader != "Sentry sentry_version=4, sentry_key=u, sentry_secret=p" { 155 | t.Error("incorrect authHeader:", client.authHeader) 156 | } 157 | } 158 | 159 | func TestNewClient(t *testing.T) { 160 | client := newClient(nil) 161 | if client.sampleRate != 1.0 { 162 | t.Error("invalid default sample rate") 163 | } 164 | } 165 | 166 | func TestSetSampleRate(t *testing.T) { 167 | client := &Client{} 168 | err := client.SetSampleRate(0.2) 169 | 170 | if err != nil { 171 | t.Error("invalid sample rate") 172 | } 173 | 174 | if client.sampleRate != 0.2 { 175 | t.Error("incorrect sample rate: ", client.sampleRate) 176 | } 177 | } 178 | 179 | func TestSetSampleRateInvalid(t *testing.T) { 180 | client := &Client{} 181 | err := client.SetSampleRate(-1.0) 182 | 183 | if err != ErrInvalidSampleRate { 184 | t.Error("invalid sample rate should return ErrInvalidSampleRate") 185 | } 186 | } 187 | 188 | func TestUnmarshalTag(t *testing.T) { 189 | actual := new(Tag) 190 | if err := json.Unmarshal([]byte(`["foo","bar"]`), actual); err != nil { 191 | t.Fatal("unable to decode JSON:", err) 192 | } 193 | 194 | expected := &Tag{Key: "foo", Value: "bar"} 195 | if !reflect.DeepEqual(actual, expected) { 196 | t.Errorf("incorrect Tag: wanted '%+v' and got '%+v'", expected, actual) 197 | } 198 | } 199 | 200 | func TestUnmarshalTags(t *testing.T) { 201 | tests := []struct { 202 | Input string 203 | Expected Tags 204 | }{ 205 | { 206 | `{"foo":"bar"}`, 207 | Tags{Tag{Key: "foo", Value: "bar"}}, 208 | }, 209 | { 210 | `[["foo","bar"],["bar","baz"]]`, 211 | Tags{Tag{Key: "foo", Value: "bar"}, Tag{Key: "bar", Value: "baz"}}, 212 | }, 213 | } 214 | 215 | for _, test := range tests { 216 | var actual Tags 217 | if err := json.Unmarshal([]byte(test.Input), &actual); err != nil { 218 | t.Fatal("unable to decode JSON:", err) 219 | } 220 | 221 | if !reflect.DeepEqual(actual, test.Expected) { 222 | t.Errorf("incorrect Tags: wanted '%+v' and got '%+v'", test.Expected, actual) 223 | } 224 | } 225 | } 226 | 227 | func TestMarshalTimestamp(t *testing.T) { 228 | timestamp := Timestamp(time.Date(2000, 01, 02, 03, 04, 05, 0, time.UTC)) 229 | expected := `"2000-01-02T03:04:05.00"` 230 | 231 | actual, err := json.Marshal(timestamp) 232 | if err != nil { 233 | t.Error(err) 234 | } 235 | 236 | if string(actual) != expected { 237 | t.Errorf("incorrect string; got %s, want %s", actual, expected) 238 | } 239 | } 240 | 241 | func TestUnmarshalTimestamp(t *testing.T) { 242 | timestamp := `"2000-01-02T03:04:05.00"` 243 | expected := Timestamp(time.Date(2000, 01, 02, 03, 04, 05, 0, time.UTC)) 244 | 245 | var actual Timestamp 246 | err := json.Unmarshal([]byte(timestamp), &actual) 247 | if err != nil { 248 | t.Error(err) 249 | } 250 | 251 | if actual != expected { 252 | t.Errorf("incorrect string; got %s, want %s", actual.Format("2006-01-02 15:04:05 -0700"), expected.Format("2006-01-02 15:04:05 -0700")) 253 | } 254 | } 255 | 256 | func TestNilClient(t *testing.T) { 257 | var client *Client 258 | eventID, ch := client.Capture(nil, nil) 259 | if eventID != "" { 260 | t.Error("expected empty eventID:", eventID) 261 | } 262 | // wait on ch: no send should succeed immediately 263 | err := <-ch 264 | if err != nil { 265 | t.Error("expected nil err:", err) 266 | } 267 | } 268 | 269 | func TestCaptureNil(t *testing.T) { 270 | var client = DefaultClient 271 | eventID, ch := client.Capture(nil, nil) 272 | if eventID != "" { 273 | t.Error("expected empty eventID:", eventID) 274 | } 275 | // wait on ch: no send should succeed immediately 276 | err := <-ch 277 | if err != nil { 278 | t.Error("expected nil err:", err) 279 | } 280 | } 281 | 282 | func TestCaptureNilError(t *testing.T) { 283 | var client = DefaultClient 284 | eventID := client.CaptureError(nil, nil) 285 | if eventID != "" { 286 | t.Error("expected empty eventID:", eventID) 287 | } 288 | } 289 | 290 | // Custom error which implements causer 291 | type customErr struct { 292 | msg string 293 | cause error 294 | } 295 | 296 | func (e *customErr) Error() (errorMsg string) { 297 | if e.msg != "" && e.cause != nil { 298 | errorMsg = fmt.Sprintf("%v \n\t==>> %v", e.msg, e.cause) 299 | } else if e.msg == "" && e.cause != nil { 300 | errorMsg = fmt.Sprintf("%v", e.cause) 301 | } else if e.msg != "" && e.cause == nil { 302 | errorMsg = fmt.Sprintf("%s", e.msg) 303 | } 304 | return 305 | } 306 | 307 | // Implementing the causer interface from github.com/pkg/errors 308 | func (e *customErr) Cause() error { 309 | return e.cause 310 | } 311 | 312 | func TestCaptureNilCauseError(t *testing.T) { 313 | var client = DefaultClient 314 | err := pkgErrors.WithStack(&customErr{ 315 | // Setting a nil cause 316 | cause: nil, 317 | msg: "This is a test", 318 | }) 319 | eventID := client.CaptureError(err, nil) 320 | if eventID == "" { 321 | t.Error("expected non-empty eventID:", eventID) 322 | } 323 | } 324 | 325 | func TestNewPacketWithExtraSetsDefault(t *testing.T) { 326 | testCases := []struct { 327 | Extra Extra 328 | Expected Extra 329 | }{ 330 | // Defaults should be set when nil is passed 331 | { 332 | Extra: nil, 333 | Expected: setExtraDefaults(Extra{}), 334 | }, 335 | // Defaults should be set when empty is passed 336 | { 337 | Extra: Extra{}, 338 | Expected: setExtraDefaults(Extra{}), 339 | }, 340 | // Packet should always override default keys 341 | { 342 | Extra: Extra{ 343 | "runtime.Version": "notagoversion", 344 | }, 345 | Expected: setExtraDefaults(Extra{}), 346 | }, 347 | // Packet should include our extra info 348 | { 349 | Extra: Extra{ 350 | "extra.extra": "extra", 351 | }, 352 | Expected: setExtraDefaults(Extra{ 353 | "extra.extra": "extra", 354 | }), 355 | }, 356 | } 357 | 358 | for i, test := range testCases { 359 | packet := NewPacketWithExtra("packet", test.Extra) 360 | if !reflect.DeepEqual(packet.Extra, test.Expected) { 361 | t.Errorf("Case [%d]: Expected packet: %+v, got: %+v", i, test.Expected, packet.Extra) 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | type causer interface { 4 | Cause() error 5 | } 6 | 7 | type errWrappedWithExtra struct { 8 | err error 9 | extraInfo map[string]interface{} 10 | } 11 | 12 | func (ewx *errWrappedWithExtra) Error() string { 13 | if ewx.err == nil { 14 | return "" 15 | } 16 | 17 | return ewx.err.Error() 18 | } 19 | 20 | func (ewx *errWrappedWithExtra) Cause() error { 21 | return ewx.err 22 | } 23 | 24 | func (ewx *errWrappedWithExtra) ExtraInfo() Extra { 25 | return ewx.extraInfo 26 | } 27 | 28 | // WrapWithExtra adds extra data to an error before reporting to Sentry 29 | func WrapWithExtra(err error, extraInfo map[string]interface{}) error { 30 | return &errWrappedWithExtra{ 31 | err: err, 32 | extraInfo: extraInfo, 33 | } 34 | } 35 | 36 | // errWithJustExtra is a regular error with just the user-provided extras added but without a cause 37 | type errWithJustExtra interface { 38 | error 39 | ExtraInfo() Extra 40 | } 41 | 42 | // ErrWithExtra links Error with attached user-provided extras that will be reported alongside the Error 43 | type ErrWithExtra interface { 44 | errWithJustExtra 45 | Cause() error 46 | } 47 | 48 | // Iteratively fetches all the Extra data added to an error, 49 | // and it's underlying errors. Extra data defined first is 50 | // respected, and is not overridden when extracting. 51 | func extractExtra(err error) Extra { 52 | extra := Extra{} 53 | 54 | currentErr := err 55 | for currentErr != nil { 56 | if errWithExtra, ok := currentErr.(errWithJustExtra); ok { 57 | for k, v := range errWithExtra.ExtraInfo() { 58 | extra[k] = v 59 | } 60 | } 61 | 62 | if errWithCause, ok := currentErr.(causer); ok { 63 | currentErr = errWithCause.Cause() 64 | } else { 65 | currentErr = nil 66 | } 67 | } 68 | 69 | return extra 70 | } 71 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestWrapWithExtraGeneratesProperErrWithExtra(t *testing.T) { 10 | errMsg := "This is bad" 11 | baseErr := fmt.Errorf(errMsg) 12 | extraInfo := map[string]interface{}{ 13 | "string": "string", 14 | "int": 1, 15 | "float": 1.001, 16 | "bool": false, 17 | } 18 | 19 | testErr := WrapWithExtra(baseErr, extraInfo) 20 | wrapped, ok := testErr.(ErrWithExtra) 21 | if !ok { 22 | t.Errorf("Wrapped error does not conform to expected protocol.") 23 | } 24 | 25 | if !reflect.DeepEqual(wrapped.Cause(), baseErr) { 26 | t.Errorf("Failed to unwrap error, got %+v, expected %+v", wrapped.Cause(), baseErr) 27 | } 28 | 29 | returnedExtra := wrapped.ExtraInfo() 30 | for expectedKey, expectedVal := range extraInfo { 31 | val, ok := returnedExtra[expectedKey] 32 | if !ok { 33 | t.Errorf("Extra data missing key: %s", expectedKey) 34 | } 35 | if val != expectedVal { 36 | t.Errorf("Extra data [%s]: Got: %+v, expected: %+v", expectedKey, val, expectedVal) 37 | } 38 | } 39 | 40 | if wrapped.Error() != errMsg { 41 | t.Errorf("Wrong error message, got: %q, expected: %q", wrapped.Error(), errMsg) 42 | } 43 | } 44 | 45 | func TestWrapWithExtraGeneratesCausableError(t *testing.T) { 46 | baseErr := fmt.Errorf("this is bad") 47 | testErr := WrapWithExtra(baseErr, nil) 48 | cause := Cause(testErr) 49 | 50 | if !reflect.DeepEqual(cause, baseErr) { 51 | t.Errorf("Failed to unwrap error, got %+v, expected %+v", cause, baseErr) 52 | } 53 | } 54 | 55 | func TestExtractErrorPullsExtraData(t *testing.T) { 56 | extraInfo := map[string]interface{}{ 57 | "string": "string", 58 | "int": 1, 59 | "float": 1.001, 60 | "bool": false, 61 | } 62 | emptyInfo := map[string]interface{}{} 63 | 64 | testCases := []struct { 65 | Error error 66 | Expected map[string]interface{} 67 | }{ 68 | // Unwrapped error shouldn't include anything 69 | { 70 | Error: fmt.Errorf("This is bad"), 71 | Expected: emptyInfo, 72 | }, 73 | // Wrapped error with nil map should extract as empty info 74 | { 75 | Error: WrapWithExtra(fmt.Errorf("This is bad"), nil), 76 | Expected: emptyInfo, 77 | }, 78 | // Wrapped error with empty map should extract as empty info 79 | { 80 | Error: WrapWithExtra(fmt.Errorf("This is bad"), emptyInfo), 81 | Expected: emptyInfo, 82 | }, 83 | // Wrapped error with extra info should extract with all data 84 | { 85 | Error: WrapWithExtra(fmt.Errorf("This is bad"), extraInfo), 86 | Expected: extraInfo, 87 | }, 88 | // Nested wrapped error should extract all the info 89 | { 90 | Error: WrapWithExtra( 91 | WrapWithExtra(fmt.Errorf("This is bad"), 92 | map[string]interface{}{ 93 | "inner": "123", 94 | }), 95 | map[string]interface{}{ 96 | "outer": "456", 97 | }, 98 | ), 99 | Expected: map[string]interface{}{ 100 | "inner": "123", 101 | "outer": "456", 102 | }, 103 | }, 104 | // Further wrapping of errors shouldn't allow for value override 105 | { 106 | Error: WrapWithExtra( 107 | WrapWithExtra(fmt.Errorf("This is bad"), 108 | map[string]interface{}{ 109 | "dontoverride": "123", 110 | }), 111 | map[string]interface{}{ 112 | "dontoverride": "456", 113 | }, 114 | ), 115 | Expected: map[string]interface{}{ 116 | "dontoverride": "123", 117 | }, 118 | }, 119 | } 120 | 121 | for i, test := range testCases { 122 | extracted := extractExtra(test.Error) 123 | if len(test.Expected) != len(extracted) { 124 | t.Errorf( 125 | "Case [%d]: Mismatched amount of data between provided and extracted extra. Got: %+v Expected: %+v", 126 | i, 127 | extracted, 128 | test.Expected, 129 | ) 130 | } 131 | 132 | for expectedKey, expectedVal := range test.Expected { 133 | val, ok := extracted[expectedKey] 134 | if !ok { 135 | t.Errorf("Case [%d]: Extra data missing key: %s", i, expectedKey) 136 | } 137 | if val != expectedVal { 138 | t.Errorf("Case [%d]: Wrong extra data for %q. Got: %+v, expected: %+v", i, expectedKey, val, expectedVal) 139 | } 140 | } 141 | } 142 | } 143 | 144 | type errorWithJustExtra struct{} 145 | 146 | func (e *errorWithJustExtra) Error() string { 147 | return "oops" 148 | } 149 | func (e *errorWithJustExtra) ExtraInfo() Extra { 150 | return Extra{"foo": "bar"} 151 | } 152 | 153 | func TestExtractExtraDoesNotRequireCause(t *testing.T) { 154 | extra := extractExtra(&errorWithJustExtra{}) 155 | if extra["foo"] != "bar" { 156 | t.Error("Could not extract extra without cause") 157 | } 158 | } 159 | 160 | func TestErrWrappedWithExtraWithNilError(t *testing.T) { 161 | ewx := WrapWithExtra(nil, map[string]interface{}{}) 162 | if errString := ewx.Error(); errString != "" { 163 | t.Errorf("Expected empty string got %s", errString) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/getsentry/raven-go" 7 | "log" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | func trace() *raven.Stacktrace { 13 | return raven.NewStacktrace(0, 2, nil) 14 | } 15 | 16 | func main() { 17 | client, err := raven.NewWithTags(os.Args[1], map[string]string{"foo": "bar"}) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | httpReq, _ := http.NewRequest("GET", "http://example.com/foo?bar=true", nil) 22 | httpReq.RemoteAddr = "127.0.0.1:80" 23 | httpReq.Header = http.Header{"Content-Type": {"text/html"}, "Content-Length": {"42"}} 24 | packet := &raven.Packet{Message: "Test report", Interfaces: []raven.Interface{raven.NewException(errors.New("example"), trace()), raven.NewHttp(httpReq)}} 25 | _, ch := client.Capture(packet, nil) 26 | if err = <-ch; err != nil { 27 | log.Fatal(err) 28 | } 29 | log.Print("sent packet successfully") 30 | } 31 | 32 | // CheckError sends error report to sentry and records event id and error name to the logs 33 | func CheckError(err error, r *http.Request) { 34 | client, err := raven.NewWithTags(os.Args[1], map[string]string{"foo": "bar"}) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | packet := raven.NewPacket(err.Error(), raven.NewException(err, trace()), raven.NewHttp(r)) 39 | eventID, _ := client.Capture(packet, nil) 40 | message := fmt.Sprintf("Error event with id \"%s\" - %s", eventID, err.Error()) 41 | log.Println(message) 42 | } 43 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func Example() { 10 | // ... i.e. raisedErr is incoming error 11 | var raisedErr error 12 | // sentry DSN generated by Sentry server 13 | var sentryDSN string 14 | // r is a request performed when error occurred 15 | var r *http.Request 16 | client, err := New(sentryDSN) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | trace := NewStacktrace(0, 2, nil) 21 | packet := NewPacket(raisedErr.Error(), NewException(raisedErr, trace), NewHttp(r)) 22 | eventID, ch := client.Capture(packet, nil) 23 | if err = <-ch; err != nil { 24 | log.Fatal(err) 25 | } 26 | message := fmt.Sprintf("Captured error with id %s: %q", eventID, raisedErr) 27 | log.Println(message) 28 | } 29 | -------------------------------------------------------------------------------- /exception.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | import ( 4 | "reflect" 5 | "regexp" 6 | ) 7 | 8 | var errorMsgPattern = regexp.MustCompile(`\A(\w+): (.+)\z`) 9 | 10 | // NewException constructs an Exception using provided Error and Stacktrace 11 | func NewException(err error, stacktrace *Stacktrace) *Exception { 12 | msg := err.Error() 13 | ex := &Exception{ 14 | Stacktrace: stacktrace, 15 | Value: msg, 16 | Type: reflect.TypeOf(err).String(), 17 | } 18 | if m := errorMsgPattern.FindStringSubmatch(msg); m != nil { 19 | ex.Module, ex.Value = m[1], m[2] 20 | } 21 | return ex 22 | } 23 | 24 | // Exception defines Sentry's spec compliant interface holding Exception information - https://docs.sentry.io/development/sdk-dev/interfaces/exception/ 25 | type Exception struct { 26 | // Required 27 | Value string `json:"value"` 28 | 29 | // Optional 30 | Type string `json:"type,omitempty"` 31 | Module string `json:"module,omitempty"` 32 | Stacktrace *Stacktrace `json:"stacktrace,omitempty"` 33 | } 34 | 35 | // Class provides name of implemented Sentry's interface 36 | func (e *Exception) Class() string { return "exception" } 37 | 38 | // Culprit tries to read top-most error message from Exception's stacktrace 39 | func (e *Exception) Culprit() string { 40 | if e.Stacktrace == nil { 41 | return "" 42 | } 43 | return e.Stacktrace.Culprit() 44 | } 45 | 46 | // Exceptions defines Sentry's spec compliant interface holding Exceptions information - https://docs.sentry.io/development/sdk-dev/interfaces/exception/ 47 | type Exceptions struct { 48 | // Required 49 | Values []*Exception `json:"values"` 50 | } 51 | 52 | // Class provides name of implemented Sentry's interface 53 | func (es Exceptions) Class() string { return "exception" } 54 | -------------------------------------------------------------------------------- /exception_test.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "testing" 7 | ) 8 | 9 | var newExceptionTests = []struct { 10 | err error 11 | Exception 12 | }{ 13 | {errors.New("foobar"), Exception{Value: "foobar", Type: "*errors.errorString"}}, 14 | {errors.New("bar: foobar"), Exception{Value: "foobar", Type: "*errors.errorString", Module: "bar"}}, 15 | } 16 | 17 | func TestNewException(t *testing.T) { 18 | for _, test := range newExceptionTests { 19 | actual := NewException(test.err, nil) 20 | if actual.Value != test.Value { 21 | t.Errorf("incorrect Value: got %s, want %s", actual.Value, test.Value) 22 | } 23 | if actual.Type != test.Type { 24 | t.Errorf("incorrect Type: got %s, want %s", actual.Type, test.Type) 25 | } 26 | if actual.Module != test.Module { 27 | t.Errorf("incorrect Module: got %s, want %s", actual.Module, test.Module) 28 | } 29 | } 30 | } 31 | 32 | func TestNewException_JSON(t *testing.T) { 33 | expected := `{"value":"foobar","type":"*errors.errorString"}` 34 | e := NewException(errors.New("foobar"), nil) 35 | b, _ := json.Marshal(e) 36 | if string(b) != expected { 37 | t.Errorf("incorrect JSON: got %s, want %s", string(b), expected) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "runtime/debug" 10 | "strings" 11 | ) 12 | 13 | // NewHttp creates new HTTP object that follows Sentry's HTTP interface spec and will be attached to the Packet 14 | func NewHttp(req *http.Request) *Http { 15 | proto := "http" 16 | if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" { 17 | proto = "https" 18 | } 19 | h := &Http{ 20 | Method: req.Method, 21 | Cookies: req.Header.Get("Cookie"), 22 | Query: sanitizeQuery(req.URL.Query()).Encode(), 23 | URL: proto + "://" + req.Host + req.URL.Path, 24 | Headers: make(map[string]string, len(req.Header)), 25 | } 26 | if addr, port, err := net.SplitHostPort(req.RemoteAddr); err == nil { 27 | h.Env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port} 28 | } 29 | for k, v := range req.Header { 30 | h.Headers[k] = strings.Join(v, ",") 31 | } 32 | h.Headers["Host"] = req.Host 33 | return h 34 | } 35 | 36 | var querySecretFields = []string{"password", "passphrase", "passwd", "secret"} 37 | 38 | func sanitizeQuery(query url.Values) url.Values { 39 | for _, keyword := range querySecretFields { 40 | for field := range query { 41 | if strings.Contains(field, keyword) { 42 | query[field] = []string{"********"} 43 | } 44 | } 45 | } 46 | return query 47 | } 48 | 49 | // Http defines Sentry's spec compliant interface holding Request information - https://docs.sentry.io/development/sdk-dev/interfaces/http/ 50 | type Http struct { 51 | // Required 52 | URL string `json:"url"` 53 | Method string `json:"method"` 54 | Query string `json:"query_string,omitempty"` 55 | 56 | // Optional 57 | Cookies string `json:"cookies,omitempty"` 58 | Headers map[string]string `json:"headers,omitempty"` 59 | Env map[string]string `json:"env,omitempty"` 60 | 61 | // Must be either a string or map[string]string 62 | Data interface{} `json:"data,omitempty"` 63 | } 64 | 65 | // Class provides name of implemented Sentry's interface 66 | func (h *Http) Class() string { return "request" } 67 | 68 | // RecoveryHandler uses Recoverer to wrap the stdlib net/http Mux. 69 | // Example: 70 | // http.HandleFunc("/", raven.RecoveryHandler(func(w http.ResponseWriter, r *http.Request) { 71 | // ... 72 | // })) 73 | func RecoveryHandler(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { 74 | return Recoverer(http.HandlerFunc(handler)).ServeHTTP 75 | } 76 | 77 | // Recoverer wraps the stdlib net/http Mux. 78 | // Example: 79 | // mux := http.NewServeMux 80 | // ... 81 | // http.Handle("/", raven.Recoverer(mux)) 82 | func Recoverer(handler http.Handler) http.Handler { 83 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 84 | defer func() { 85 | if rval := recover(); rval != nil { 86 | debug.PrintStack() 87 | rvalStr := fmt.Sprint(rval) 88 | var packet *Packet 89 | if err, ok := rval.(error); ok { 90 | packet = NewPacket(rvalStr, NewException(errors.New(rvalStr), GetOrNewStacktrace(err, 2, 3, nil)), NewHttp(r)) 91 | } else { 92 | packet = NewPacket(rvalStr, NewException(errors.New(rvalStr), NewStacktrace(2, 3, nil)), NewHttp(r)) 93 | } 94 | Capture(packet, nil) 95 | w.WriteHeader(http.StatusInternalServerError) 96 | } 97 | }() 98 | 99 | handler.ServeHTTP(w, r) 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | type Testcase struct { 11 | request *http.Request 12 | *Http 13 | } 14 | 15 | func newBaseRequest() *http.Request { 16 | u, _ := url.Parse("http://example.com/") 17 | header := make(http.Header) 18 | header.Add("Foo", "bar") 19 | 20 | req := &http.Request{ 21 | Method: "GET", 22 | URL: u, 23 | Proto: "HTTP/1.1", 24 | ProtoMajor: 1, 25 | ProtoMinor: 1, 26 | Header: header, 27 | Host: u.Host, 28 | RemoteAddr: "127.0.0.1:8000", 29 | } 30 | return req 31 | } 32 | 33 | func newBaseHttp() *Http { 34 | h := &Http{ 35 | Method: "GET", 36 | Cookies: "", 37 | Query: "", 38 | URL: "http://example.com/", 39 | Headers: map[string]string{"Foo": "bar"}, 40 | Env: map[string]string{"REMOTE_ADDR": "127.0.0.1", "REMOTE_PORT": "8000"}, 41 | } 42 | h.Headers["Host"] = "example.com" 43 | return h 44 | } 45 | 46 | func NewRequest() Testcase { 47 | return Testcase{newBaseRequest(), newBaseHttp()} 48 | } 49 | 50 | func NewRequestIPV6() Testcase { 51 | req := newBaseRequest() 52 | req.RemoteAddr = "[:1]:8000" 53 | 54 | h := newBaseHttp() 55 | h.Env = map[string]string{"REMOTE_ADDR": ":1", "REMOTE_PORT": "8000"} 56 | return Testcase{req, h} 57 | } 58 | 59 | func NewRequestMultipleHeaders() Testcase { 60 | req := newBaseRequest() 61 | req.Header.Add("Foo", "baz") 62 | 63 | h := newBaseHttp() 64 | h.Headers["Foo"] = "bar,baz" 65 | return Testcase{req, h} 66 | } 67 | 68 | func NewSecureRequest() Testcase { 69 | req := newBaseRequest() 70 | req.Header.Add("X-Forwarded-Proto", "https") 71 | 72 | h := newBaseHttp() 73 | h.URL = "https://example.com/" 74 | h.Headers["X-Forwarded-Proto"] = "https" 75 | return Testcase{req, h} 76 | } 77 | 78 | func NewCookiesRequest() Testcase { 79 | val := "foo=bar; bar=baz" 80 | req := newBaseRequest() 81 | req.Header.Add("Cookie", val) 82 | 83 | h := newBaseHttp() 84 | h.Cookies = val 85 | h.Headers["Cookie"] = val 86 | return Testcase{req, h} 87 | } 88 | 89 | var newHttpTests = []Testcase{ 90 | NewRequest(), 91 | NewRequestIPV6(), 92 | NewRequestMultipleHeaders(), 93 | NewSecureRequest(), 94 | NewCookiesRequest(), 95 | } 96 | 97 | func TestNewHttp(t *testing.T) { 98 | for _, test := range newHttpTests { 99 | actual := NewHttp(test.request) 100 | if actual.Method != test.Method { 101 | t.Errorf("incorrect Method: got %s, want %s", actual.Method, test.Method) 102 | } 103 | if actual.Cookies != test.Cookies { 104 | t.Errorf("incorrect Cookies: got %s, want %s", actual.Cookies, test.Cookies) 105 | } 106 | if actual.Query != test.Query { 107 | t.Errorf("incorrect Query: got %s, want %s", actual.Query, test.Query) 108 | } 109 | if actual.URL != test.URL { 110 | t.Errorf("incorrect URL: got %s, want %s", actual.URL, test.URL) 111 | } 112 | if !reflect.DeepEqual(actual.Headers, test.Headers) { 113 | t.Errorf("incorrect Headers: got %+v, want %+v", actual.Headers, test.Headers) 114 | } 115 | if !reflect.DeepEqual(actual.Env, test.Env) { 116 | t.Errorf("incorrect Env: got %+v, want %+v", actual.Env, test.Env) 117 | } 118 | if !reflect.DeepEqual(actual.Data, test.Data) { 119 | t.Errorf("incorrect Data: got %+v, want %+v", actual.Data, test.Data) 120 | } 121 | } 122 | } 123 | 124 | var sanitizeQueryTests = []struct { 125 | input, output string 126 | }{ 127 | {"foo=bar", "foo=bar"}, 128 | {"password=foo", "password=********"}, 129 | {"passphrase=foo", "passphrase=********"}, 130 | {"passwd=foo", "passwd=********"}, 131 | {"secret=foo", "secret=********"}, 132 | {"secretstuff=foo", "secretstuff=********"}, 133 | {"foo=bar&secret=foo", "foo=bar&secret=********"}, 134 | {"secret=foo&secret=bar", "secret=********"}, 135 | } 136 | 137 | func parseQuery(q string) url.Values { 138 | r, _ := url.ParseQuery(q) 139 | return r 140 | } 141 | 142 | func TestSanitizeQuery(t *testing.T) { 143 | for _, test := range sanitizeQueryTests { 144 | actual := sanitizeQuery(parseQuery(test.input)) 145 | expected := parseQuery(test.output) 146 | if !reflect.DeepEqual(actual, expected) { 147 | t.Errorf("incorrect sanitization: got %+v, want %+v", actual, expected) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /interfaces.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | // Message defines Sentry's spec compliant interface holding Message information - https://docs.sentry.io/development/sdk-dev/interfaces/message/ 4 | type Message struct { 5 | // Required 6 | Message string `json:"message"` 7 | 8 | // Optional 9 | Params []interface{} `json:"params,omitempty"` 10 | } 11 | 12 | // Class provides name of implemented Sentry's interface 13 | func (m *Message) Class() string { return "logentry" } 14 | 15 | // Template defines Sentry's spec compliant interface holding Template information - https://docs.sentry.io/development/sdk-dev/interfaces/template/ 16 | type Template struct { 17 | // Required 18 | Filename string `json:"filename"` 19 | Lineno int `json:"lineno"` 20 | ContextLine string `json:"context_line"` 21 | 22 | // Optional 23 | PreContext []string `json:"pre_context,omitempty"` 24 | PostContext []string `json:"post_context,omitempty"` 25 | AbsolutePath string `json:"abs_path,omitempty"` 26 | } 27 | 28 | // Class provides name of implemented Sentry's interface 29 | func (t *Template) Class() string { return "template" } 30 | 31 | // User defines Sentry's spec compliant interface holding User information - https://docs.sentry.io/development/sdk-dev/interfaces/user/ 32 | type User struct { 33 | // All fields are optional 34 | ID string `json:"id,omitempty"` 35 | Username string `json:"username,omitempty"` 36 | Email string `json:"email,omitempty"` 37 | IP string `json:"ip_address,omitempty"` 38 | } 39 | 40 | // Class provides name of implemented Sentry's interface 41 | func (h *User) Class() string { return "user" } 42 | 43 | // Query defines Sentry's spec compliant interface holding Context information - https://docs.sentry.io/development/sdk-dev/interfaces/contexts/ 44 | type Query struct { 45 | // Required 46 | Query string `json:"query"` 47 | 48 | // Optional 49 | Engine string `json:"engine,omitempty"` 50 | } 51 | 52 | // Class provides name of implemented Sentry's interface 53 | func (q *Query) Class() string { return "query" } 54 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | go test -race ./... 3 | go test -cover ./... 4 | go test -v ./... 5 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | go get golang.org/x/lint/golint 5 | 6 | # Http => HTTP change would require a major version bump as all those functions/types are exported 7 | if [[ -z "$(golint | grep -vE \"Http.*should be.*HTTP\")" ]]; then 8 | exit 0 9 | else 10 | echo "Failed golint command" 11 | exit 1 12 | fi 13 | -------------------------------------------------------------------------------- /stacktrace.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // Some code from the runtime/debug package of the Go standard library. 5 | 6 | package raven 7 | 8 | import ( 9 | "bytes" 10 | "go/build" 11 | "io/ioutil" 12 | "path/filepath" 13 | "runtime" 14 | "strings" 15 | "sync" 16 | ) 17 | 18 | // Stacktrace defines Sentry's spec compliant interface holding Stacktrace information - https://docs.sentry.io/development/sdk-dev/interfaces/stacktrace/ 19 | type Stacktrace struct { 20 | // Required 21 | Frames []*StacktraceFrame `json:"frames"` 22 | } 23 | 24 | // Class provides name of implemented Sentry's interface 25 | func (s *Stacktrace) Class() string { return "stacktrace" } 26 | 27 | // Culprit iterates through stacktrace frames and returns first in-app frame's information 28 | func (s *Stacktrace) Culprit() string { 29 | for i := len(s.Frames) - 1; i >= 0; i-- { 30 | frame := s.Frames[i] 31 | if frame.InApp == true && frame.Module != "" && frame.Function != "" { 32 | return frame.Module + "." + frame.Function 33 | } 34 | } 35 | return "" 36 | } 37 | 38 | // StacktraceFrame defines Sentry's spec compliant interface holding Frame information - https://docs.sentry.io/development/sdk-dev/interfaces/stacktrace/ 39 | type StacktraceFrame struct { 40 | // At least one required 41 | Filename string `json:"filename,omitempty"` 42 | Function string `json:"function,omitempty"` 43 | Module string `json:"module,omitempty"` 44 | 45 | // Optional 46 | Lineno int `json:"lineno,omitempty"` 47 | Colno int `json:"colno,omitempty"` 48 | AbsolutePath string `json:"abs_path,omitempty"` 49 | ContextLine string `json:"context_line,omitempty"` 50 | PreContext []string `json:"pre_context,omitempty"` 51 | PostContext []string `json:"post_context,omitempty"` 52 | InApp bool `json:"in_app"` 53 | } 54 | 55 | // GetOrNewStacktrace tries to get stacktrace from err as an interface of github.com/pkg/errors, or else NewStacktrace() 56 | func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []string) *Stacktrace { 57 | type stackTracer interface { 58 | StackTrace() []runtime.Frame 59 | } 60 | stacktrace, ok := err.(stackTracer) 61 | if !ok { 62 | return NewStacktrace(skip+1, context, appPackagePrefixes) 63 | } 64 | var frames []*StacktraceFrame 65 | for _, f := range stacktrace.StackTrace() { 66 | pc := uintptr(f.PC) - 1 67 | fn := runtime.FuncForPC(pc) 68 | var fName string 69 | var file string 70 | var line int 71 | if fn != nil { 72 | file, line = fn.FileLine(pc) 73 | fName = fn.Name() 74 | } else { 75 | file = "unknown" 76 | fName = "unknown" 77 | } 78 | frame := NewStacktraceFrame(pc, fName, file, line, context, appPackagePrefixes) 79 | if frame != nil { 80 | frames = append([]*StacktraceFrame{frame}, frames...) 81 | } 82 | } 83 | return &Stacktrace{Frames: frames} 84 | } 85 | 86 | // NewStacktrace intializes and populates a new stacktrace, skipping skip frames. 87 | // 88 | // context is the number of surrounding lines that should be included for context. 89 | // Setting context to 3 would try to get seven lines. Setting context to -1 returns 90 | // one line with no surrounding context, and 0 returns no context. 91 | // 92 | // appPackagePrefixes is a list of prefixes used to check whether a package should 93 | // be considered "in app". 94 | func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace { 95 | var frames []*StacktraceFrame 96 | 97 | callerPcs := make([]uintptr, 100) 98 | numCallers := runtime.Callers(skip+2, callerPcs) 99 | 100 | // If there are no callers, the entire stacktrace is nil 101 | if numCallers == 0 { 102 | return nil 103 | } 104 | 105 | callersFrames := runtime.CallersFrames(callerPcs) 106 | 107 | for { 108 | fr, more := callersFrames.Next() 109 | frame := NewStacktraceFrame(fr.PC, fr.Function, fr.File, fr.Line, context, appPackagePrefixes) 110 | if frame != nil { 111 | frames = append(frames, frame) 112 | } 113 | if !more { 114 | break 115 | } 116 | } 117 | // If there are no frames, the entire stacktrace is nil 118 | if len(frames) == 0 { 119 | return nil 120 | } 121 | // Optimize the path where there's only 1 frame 122 | if len(frames) == 1 { 123 | return &Stacktrace{frames} 124 | } 125 | // Sentry wants the frames with the oldest first, so reverse them 126 | for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 { 127 | frames[i], frames[j] = frames[j], frames[i] 128 | } 129 | return &Stacktrace{frames} 130 | } 131 | 132 | // NewStacktraceFrame builds a single frame using data returned from runtime.Caller. 133 | // 134 | // context is the number of surrounding lines that should be included for context. 135 | // Setting context to 3 would try to get seven lines. Setting context to -1 returns 136 | // one line with no surrounding context, and 0 returns no context. 137 | // 138 | // appPackagePrefixes is a list of prefixes used to check whether a package should 139 | // be considered "in app". 140 | func NewStacktraceFrame(pc uintptr, fName, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame { 141 | frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line} 142 | frame.Module, frame.Function = functionName(fName) 143 | frame.InApp = isInAppFrame(*frame, appPackagePrefixes) 144 | 145 | // `runtime.goexit` is effectively a placeholder that comes from 146 | // runtime/asm_amd64.s and is meaningless. 147 | if frame.Module == "runtime" && frame.Function == "goexit" { 148 | return nil 149 | } 150 | 151 | if context > 0 { 152 | contextLines, lineIdx := sourceCodeLoader.Load(file, line, context) 153 | if len(contextLines) > 0 { 154 | for i, line := range contextLines { 155 | switch { 156 | case i < lineIdx: 157 | frame.PreContext = append(frame.PreContext, string(line)) 158 | case i == lineIdx: 159 | frame.ContextLine = string(line) 160 | default: 161 | frame.PostContext = append(frame.PostContext, string(line)) 162 | } 163 | } 164 | } 165 | } else if context == -1 { 166 | contextLine, _ := sourceCodeLoader.Load(file, line, 0) 167 | if len(contextLine) > 0 { 168 | frame.ContextLine = string(contextLine[0]) 169 | } 170 | } 171 | 172 | return frame 173 | } 174 | 175 | // Determines whether frame should be marked as InApp 176 | func isInAppFrame(frame StacktraceFrame, appPackagePrefixes []string) bool { 177 | if frame.Module == "main" { 178 | return true 179 | } 180 | for _, prefix := range appPackagePrefixes { 181 | if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") { 182 | return true 183 | } 184 | } 185 | return false 186 | } 187 | 188 | // Retrieve the name of the package and function containing the PC. 189 | func functionName(fName string) (pack string, name string) { 190 | name = fName 191 | // We get this: 192 | // runtime/debug.*T·ptrmethod 193 | // and want this: 194 | // pack = runtime/debug 195 | // name = *T.ptrmethod 196 | if idx := strings.LastIndex(name, "."); idx != -1 { 197 | pack = name[:idx] 198 | name = name[idx+1:] 199 | } 200 | name = strings.Replace(name, "·", ".", -1) 201 | return 202 | } 203 | 204 | // SourceCodeLoader allows to read source code files from the current fs 205 | type SourceCodeLoader interface { 206 | Load(filename string, line, context int) ([][]byte, int) 207 | } 208 | 209 | var sourceCodeLoader SourceCodeLoader = &fsLoader{cache: make(map[string][][]byte)} 210 | 211 | // SetSourceCodeLoader overrides currently used loader for the new one 212 | func SetSourceCodeLoader(loader SourceCodeLoader) { 213 | sourceCodeLoader = loader 214 | } 215 | 216 | type fsLoader struct { 217 | mu sync.Mutex 218 | cache map[string][][]byte 219 | } 220 | 221 | func (fs *fsLoader) Load(filename string, line, context int) ([][]byte, int) { 222 | fs.mu.Lock() 223 | defer fs.mu.Unlock() 224 | lines, ok := fs.cache[filename] 225 | if !ok { 226 | data, err := ioutil.ReadFile(filename) 227 | if err != nil { 228 | // cache errors as nil slice: code below handles it correctly 229 | // otherwise when missing the source or running as a different user, we try 230 | // reading the file on each error which is unnecessary 231 | fs.cache[filename] = nil 232 | return nil, 0 233 | } 234 | lines = bytes.Split(data, []byte{'\n'}) 235 | fs.cache[filename] = lines 236 | } 237 | 238 | if lines == nil { 239 | // cached error from ReadFile: return no lines 240 | return nil, 0 241 | } 242 | 243 | line-- // stack trace lines are 1-indexed 244 | start := line - context 245 | var idx int 246 | if start < 0 { 247 | start = 0 248 | idx = line 249 | } else { 250 | idx = context 251 | } 252 | end := line + context + 1 253 | if line >= len(lines) { 254 | return nil, 0 255 | } 256 | if end > len(lines) { 257 | end = len(lines) 258 | } 259 | return lines[start:end], idx 260 | } 261 | 262 | var trimPaths []string 263 | 264 | // Try to trim the GOROOT or GOPATH prefix off of a filename 265 | func trimPath(filename string) string { 266 | for _, prefix := range trimPaths { 267 | if trimmed := strings.TrimPrefix(filename, prefix); len(trimmed) < len(filename) { 268 | return trimmed 269 | } 270 | } 271 | return filename 272 | } 273 | 274 | func init() { 275 | // Collect all source directories, and make sure they 276 | // end in a trailing "separator" 277 | for _, prefix := range build.Default.SrcDirs() { 278 | if prefix[len(prefix)-1] != filepath.Separator { 279 | prefix += string(filepath.Separator) 280 | } 281 | trimPaths = append(trimPaths, prefix) 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /stacktrace_test.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | // a 15 | func trace() *Stacktrace { 16 | return NewStacktrace(0, 2, []string{thisPackage}) 17 | // b 18 | } 19 | 20 | func init() { 21 | thisFile, thisPackage = derivePackage() 22 | functionNameTests = []FunctionNameTest{ 23 | {0, thisPackage, "TestFunctionName"}, 24 | {1, "testing", "tRunner"}, 25 | {2, "runtime", "goexit"}, 26 | {100, "", ""}, 27 | } 28 | } 29 | 30 | type FunctionNameTest struct { 31 | skip int 32 | pack string 33 | name string 34 | } 35 | 36 | var ( 37 | thisFile string 38 | thisPackage string 39 | functionNameTests []FunctionNameTest 40 | ) 41 | 42 | func TestFunctionName(t *testing.T) { 43 | for _, test := range functionNameTests { 44 | pc, _, _, _ := runtime.Caller(test.skip) 45 | pack, name := functionName(runtime.FuncForPC(pc).Name()) 46 | 47 | if pack != test.pack { 48 | t.Errorf("incorrect package; got %s, want %s", pack, test.pack) 49 | } 50 | if name != test.name { 51 | t.Errorf("incorrect function; got %s, want %s", name, test.name) 52 | } 53 | } 54 | } 55 | 56 | func TestStacktrace(t *testing.T) { 57 | st := trace() 58 | if st == nil { 59 | t.Error("got nil stacktrace") 60 | } 61 | if len(st.Frames) == 0 { 62 | t.Error("got zero frames") 63 | } 64 | } 65 | 66 | func TestStacktraceFrame(t *testing.T) { 67 | st := trace() 68 | f := st.Frames[len(st.Frames)-1] 69 | _, filename, _, _ := runtime.Caller(0) 70 | runningInVendored := strings.Contains(filename, "vendor") 71 | 72 | if f.Filename != thisFile { 73 | t.Errorf("incorrect Filename; got %s, want %s", f.Filename, thisFile) 74 | } 75 | if !strings.HasSuffix(f.AbsolutePath, thisFile) { 76 | t.Error("incorrect AbsolutePath:", f.AbsolutePath) 77 | } 78 | if f.Function != "trace" { 79 | t.Error("incorrect Function:", f.Function) 80 | } 81 | if f.Module != thisPackage { 82 | t.Error("incorrect Module:", f.Module) 83 | } 84 | if f.Lineno != 16 { 85 | t.Error("incorrect Lineno:", f.Lineno) 86 | } 87 | if f.InApp != !runningInVendored { 88 | t.Error("expected InApp to be true") 89 | } 90 | if f.InApp && st.Culprit() != fmt.Sprintf("%s.trace", thisPackage) { 91 | t.Error("incorrect Culprit:", st.Culprit()) 92 | } 93 | } 94 | 95 | func TestStacktraceContext(t *testing.T) { 96 | st := trace() 97 | f := st.Frames[len(st.Frames)-1] 98 | if f.ContextLine != "\treturn NewStacktrace(0, 2, []string{thisPackage})" { 99 | t.Errorf("incorrect ContextLine: %#v", f.ContextLine) 100 | } 101 | if len(f.PreContext) != 2 || f.PreContext[0] != "// a" || f.PreContext[1] != "func trace() *Stacktrace {" { 102 | t.Errorf("incorrect PreContext %#v", f.PreContext) 103 | } 104 | if len(f.PostContext) != 2 || f.PostContext[0] != "\t// b" || f.PostContext[1] != "}" { 105 | t.Errorf("incorrect PostContext %#v", f.PostContext) 106 | } 107 | } 108 | 109 | func derivePackage() (file, pack string) { 110 | // Get file name by seeking caller's file name. 111 | _, callerFile, _, ok := runtime.Caller(1) 112 | if !ok { 113 | return 114 | } 115 | 116 | // Trim file name 117 | file = callerFile 118 | for _, dir := range build.Default.SrcDirs() { 119 | dir := dir + string(filepath.Separator) 120 | if trimmed := strings.TrimPrefix(callerFile, dir); len(trimmed) < len(file) { 121 | file = trimmed 122 | } 123 | } 124 | 125 | // Now derive package name 126 | dir := filepath.Dir(callerFile) 127 | 128 | dirPkg, err := build.ImportDir(dir, build.AllowBinary) 129 | if err != nil { 130 | return 131 | } 132 | 133 | pack = dirPkg.ImportPath 134 | return 135 | } 136 | 137 | // TestNewStacktrace_outOfBounds verifies that a context exceeding the number 138 | // of lines in a file does not cause a panic. 139 | func TestNewStacktrace_outOfBounds(t *testing.T) { 140 | st := NewStacktrace(0, 1000000, []string{thisPackage}) 141 | f := st.Frames[len(st.Frames)-1] 142 | if f.ContextLine != "\tst := NewStacktrace(0, 1000000, []string{thisPackage})" { 143 | t.Errorf("incorrect ContextLine: %#v", f.ContextLine) 144 | } 145 | } 146 | 147 | func TestNewStacktrace_noFrames(t *testing.T) { 148 | st := NewStacktrace(999999999, 0, []string{}) 149 | if st != nil { 150 | t.Errorf("expected st.Frames to be nil: %v", st) 151 | } 152 | } 153 | 154 | func TestFileContext(t *testing.T) { 155 | // reset the cache 156 | loader := &fsLoader{cache: make(map[string][][]byte)} 157 | 158 | tempdir, err := ioutil.TempDir("", "") 159 | if err != nil { 160 | t.Fatal("failed to create temporary directory:", err) 161 | } 162 | 163 | defer func() { 164 | err := os.RemoveAll(tempdir) 165 | if err != nil { 166 | fmt.Println("failed to remove temporary directory:", err) 167 | } 168 | }() 169 | 170 | okPath := filepath.Join(tempdir, "ok") 171 | missingPath := filepath.Join(tempdir, "missing") 172 | noPermissionPath := filepath.Join(tempdir, "noperms") 173 | 174 | err = ioutil.WriteFile(okPath, []byte("hello\nworld\n"), 0600) 175 | if err != nil { 176 | t.Fatal("failed writing file:", err) 177 | } 178 | err = ioutil.WriteFile(noPermissionPath, []byte("no access\n"), 0000) 179 | if err != nil { 180 | t.Fatal("failed writing file:", err) 181 | } 182 | 183 | tests := []struct { 184 | path string 185 | expectedLines int 186 | expectedIndex int 187 | }{ 188 | {okPath, 1, 0}, 189 | {missingPath, 0, 0}, 190 | {noPermissionPath, 0, 0}, 191 | } 192 | for i, test := range tests { 193 | lines, index := loader.Load(test.path, 1, 0) 194 | if !(len(lines) == test.expectedLines && index == test.expectedIndex) { 195 | t.Errorf("%d: fileContext(%#v, 1, 0) = %v, %v; expected len()=%d, %d", 196 | i, test.path, lines, index, test.expectedLines, test.expectedIndex) 197 | } 198 | cacheLen := len(loader.cache) 199 | if cacheLen != i+1 { 200 | t.Errorf("%d: result was not cached; len=%d", i, cacheLen) 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package raven 2 | 3 | // Writer holds all the information about the message that will be reported to Sentry 4 | type Writer struct { 5 | Client *Client 6 | Level Severity 7 | Logger string // Logger name reported to Sentry 8 | } 9 | 10 | // Write formats the byte slice p into a string, and sends a message to 11 | // Sentry at the severity level indicated by the Writer w. 12 | func (w *Writer) Write(p []byte) (int, error) { 13 | message := string(p) 14 | 15 | packet := NewPacket(message, &Message{message, nil}) 16 | packet.Level = w.Level 17 | packet.Logger = w.Logger 18 | w.Client.Capture(packet, nil) 19 | 20 | return len(p), nil 21 | } 22 | --------------------------------------------------------------------------------