pax_global_header00006660000000000000000000000064133776111720014522gustar00rootroot0000000000000052 comment=f04e7487e9a6b9d9837d52743fb5f40576c56411 raven-go-0.2.0/000077500000000000000000000000001337761117200132375ustar00rootroot00000000000000raven-go-0.2.0/.dockerignore000066400000000000000000000000051337761117200157060ustar00rootroot00000000000000.git raven-go-0.2.0/.gitignore000066400000000000000000000000661337761117200152310ustar00rootroot00000000000000*.test *.out example/example /xunit.xml /coverage.xml raven-go-0.2.0/.gitmodules000066400000000000000000000000001337761117200154020ustar00rootroot00000000000000raven-go-0.2.0/.travis.yml000066400000000000000000000017201337761117200153500ustar00rootroot00000000000000sudo: false language: go go: - 1.7.x - 1.8.x - 1.9.x - 1.10.x - 1.11.x - tip before_install: - go install -race std - go get golang.org/x/tools/cmd/cover - go get github.com/tebeka/go2xunit - go get github.com/t-yuki/gocover-cobertura - go get -v ./... script: - go test -v -race ./... | tee gotest.out - $GOPATH/bin/go2xunit -fail -input gotest.out -output xunit.xml - go test -v -coverprofile=coverage.txt -covermode count . - $GOPATH/bin/gocover-cobertura < coverage.txt > coverage.xml after_script: - npm install -g @zeus-ci/cli - zeus upload -t "application/x-cobertura+xml" coverage.xml - zeus upload -t "application/x-xunit+xml" xunit.xml matrix: allow_failures: - go: tip notifications: webhooks: urls: - https://zeus.ci/hooks/cd949996-d30a-11e8-ba53-0a580a28042d/public/provider/travis/webhook on_success: always on_failure: always on_start: always on_cancel: always on_error: always raven-go-0.2.0/Dockerfile.test000066400000000000000000000004341337761117200162100ustar00rootroot00000000000000FROM golang:1.7 RUN mkdir -p /go/src/github.com/getsentry/raven-go WORKDIR /go/src/github.com/getsentry/raven-go ENV GOPATH /go RUN go install -race std && go get golang.org/x/tools/cmd/cover COPY . /go/src/github.com/getsentry/raven-go RUN go get -v ./... CMD ["./runtests.sh"] raven-go-0.2.0/LICENSE000066400000000000000000000030321337761117200142420ustar00rootroot00000000000000Copyright (c) 2013 Apollic Software, LLC. All rights reserved. Copyright (c) 2015 Functional Software, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Apollic Software, LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. raven-go-0.2.0/README.md000066400000000000000000000013641337761117200145220ustar00rootroot00000000000000# raven [![Build Status](https://api.travis-ci.org/getsentry/raven-go.svg?branch=master)](https://travis-ci.org/getsentry/raven-go) [![Go Report Card](https://goreportcard.com/badge/github.com/getsentry/raven-go)](https://goreportcard.com/report/github.com/getsentry/raven-go) [![GoDoc](https://godoc.org/github.com/getsentry/raven-go?status.svg)](https://godoc.org/github.com/getsentry/raven-go) raven is the official Go SDK for the [Sentry](https://github.com/getsentry/sentry) event/error logging system. - [**API Documentation**](https://godoc.org/github.com/getsentry/raven-go) - [**Usage and Examples**](https://docs.sentry.io/clients/go/) ## Installation ```text go get github.com/getsentry/raven-go ``` Note: Go 1.7 and newer are supported. raven-go-0.2.0/client.go000066400000000000000000000633001337761117200150460ustar00rootroot00000000000000// Package raven implements a client for the Sentry error logging service. package raven import ( "bytes" "compress/zlib" "crypto/rand" "crypto/tls" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" mrand "math/rand" "net/http" "net/url" "os" "regexp" "runtime" "strings" "sync" "time" "github.com/certifi/gocertifi" pkgErrors "github.com/pkg/errors" ) const ( userAgent = "raven-go/1.0" timestampFormat = `"2006-01-02T15:04:05.00"` ) var ( ErrPacketDropped = errors.New("raven: packet dropped") ErrUnableToUnmarshalJSON = errors.New("raven: unable to unmarshal JSON") ErrMissingUser = errors.New("raven: dsn missing public key and/or password") ErrMissingProjectID = errors.New("raven: dsn missing project id") ErrInvalidSampleRate = errors.New("raven: sample rate should be between 0 and 1") ) type Severity string // http://docs.python.org/2/howto/logging.html#logging-levels const ( DEBUG = Severity("debug") INFO = Severity("info") WARNING = Severity("warning") ERROR = Severity("error") FATAL = Severity("fatal") ) type Timestamp time.Time func (t Timestamp) MarshalJSON() ([]byte, error) { return []byte(time.Time(t).UTC().Format(timestampFormat)), nil } func (timestamp *Timestamp) UnmarshalJSON(data []byte) error { t, err := time.Parse(timestampFormat, string(data)) if err != nil { return err } *timestamp = Timestamp(t) return nil } func (timestamp Timestamp) Format(format string) string { t := time.Time(timestamp) return t.Format(format) } // An Interface is a Sentry interface that will be serialized as JSON. // It must implement json.Marshaler or use json struct tags. type Interface interface { // The Sentry class name. Example: sentry.interfaces.Stacktrace Class() string } type Culpriter interface { Culprit() string } type Transport interface { Send(url, authHeader string, packet *Packet) error } type Extra map[string]interface{} type outgoingPacket struct { packet *Packet ch chan error } type Tag struct { Key string Value string } type Tags []Tag func (tag *Tag) MarshalJSON() ([]byte, error) { return json.Marshal([2]string{tag.Key, tag.Value}) } func (t *Tag) UnmarshalJSON(data []byte) error { var tag [2]string if err := json.Unmarshal(data, &tag); err != nil { return err } *t = Tag{tag[0], tag[1]} return nil } func (t *Tags) UnmarshalJSON(data []byte) error { var tags []Tag switch data[0] { case '[': // Unmarshal into []Tag if err := json.Unmarshal(data, &tags); err != nil { return err } case '{': // Unmarshal into map[string]string tagMap := make(map[string]string) if err := json.Unmarshal(data, &tagMap); err != nil { return err } // Convert to []Tag for k, v := range tagMap { tags = append(tags, Tag{k, v}) } default: return ErrUnableToUnmarshalJSON } *t = tags return nil } // https://docs.getsentry.com/hosted/clientdev/#building-the-json-packet type Packet struct { // Required Message string `json:"message"` // Required, set automatically by Client.Send/Report via Packet.Init if blank EventID string `json:"event_id"` Project string `json:"project"` Timestamp Timestamp `json:"timestamp"` Level Severity `json:"level"` Logger string `json:"logger"` // Optional Platform string `json:"platform,omitempty"` Culprit string `json:"culprit,omitempty"` ServerName string `json:"server_name,omitempty"` Release string `json:"release,omitempty"` Environment string `json:"environment,omitempty"` Tags Tags `json:"tags,omitempty"` Modules map[string]string `json:"modules,omitempty"` Fingerprint []string `json:"fingerprint,omitempty"` Extra Extra `json:"extra,omitempty"` Interfaces []Interface `json:"-"` } // NewPacket constructs a packet with the specified message and interfaces. func NewPacket(message string, interfaces ...Interface) *Packet { extra := Extra{} setExtraDefaults(extra) return &Packet{ Message: message, Interfaces: interfaces, Extra: extra, } } // NewPacketWithExtra constructs a packet with the specified message, extra information, and interfaces. func NewPacketWithExtra(message string, extra Extra, interfaces ...Interface) *Packet { if extra == nil { extra = Extra{} } setExtraDefaults(extra) return &Packet{ Message: message, Interfaces: interfaces, Extra: extra, } } func setExtraDefaults(extra Extra) Extra { extra["runtime.Version"] = runtime.Version() extra["runtime.NumCPU"] = runtime.NumCPU() extra["runtime.GOMAXPROCS"] = runtime.GOMAXPROCS(0) // 0 just returns the current value extra["runtime.NumGoroutine"] = runtime.NumGoroutine() return extra } // Init initializes required fields in a packet. It is typically called by // Client.Send/Report automatically. func (packet *Packet) Init(project string) error { if packet.Project == "" { packet.Project = project } if packet.EventID == "" { var err error packet.EventID, err = uuid() if err != nil { return err } } if time.Time(packet.Timestamp).IsZero() { packet.Timestamp = Timestamp(time.Now()) } if packet.Level == "" { packet.Level = ERROR } if packet.Logger == "" { packet.Logger = "root" } if packet.ServerName == "" { packet.ServerName = hostname } if packet.Platform == "" { packet.Platform = "go" } if packet.Culprit == "" { for _, inter := range packet.Interfaces { if c, ok := inter.(Culpriter); ok { packet.Culprit = c.Culprit() if packet.Culprit != "" { break } } } } return nil } func (packet *Packet) AddTags(tags map[string]string) { for k, v := range tags { packet.Tags = append(packet.Tags, Tag{k, v}) } } func uuid() (string, error) { id := make([]byte, 16) _, err := io.ReadFull(rand.Reader, id) if err != nil { return "", err } id[6] &= 0x0F // clear version id[6] |= 0x40 // set version to 4 (random uuid) id[8] &= 0x3F // clear variant id[8] |= 0x80 // set to IETF variant return hex.EncodeToString(id), nil } func (packet *Packet) JSON() ([]byte, error) { packetJSON, err := json.Marshal(packet) if err != nil { return nil, err } interfaces := make(map[string]Interface, len(packet.Interfaces)) for _, inter := range packet.Interfaces { if inter != nil { interfaces[inter.Class()] = inter } } if len(interfaces) > 0 { interfaceJSON, err := json.Marshal(interfaces) if err != nil { return nil, err } packetJSON[len(packetJSON)-1] = ',' packetJSON = append(packetJSON, interfaceJSON[1:]...) } return packetJSON, nil } type context struct { user *User http *Http tags map[string]string } func (c *context) setUser(u *User) { c.user = u } func (c *context) setHttp(h *Http) { c.http = h } func (c *context) setTags(t map[string]string) { if c.tags == nil { c.tags = make(map[string]string) } for k, v := range t { c.tags[k] = v } } func (c *context) clear() { c.user = nil c.http = nil c.tags = nil } // Return a list of interfaces to be used in appending with the rest func (c *context) interfaces() []Interface { len, i := 0, 0 if c.user != nil { len++ } if c.http != nil { len++ } interfaces := make([]Interface, len) if c.user != nil { interfaces[i] = c.user i++ } if c.http != nil { interfaces[i] = c.http i++ } return interfaces } // The maximum number of packets that will be buffered waiting to be delivered. // Packets will be dropped if the buffer is full. Used by NewClient. var MaxQueueBuffer = 100 func newTransport() Transport { t := &HTTPTransport{} rootCAs, err := gocertifi.CACerts() if err != nil { log.Println("raven: failed to load root TLS certificates:", err) } else { t.Client = &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{RootCAs: rootCAs}, }, } } return t } func newClient(tags map[string]string) *Client { client := &Client{ Transport: newTransport(), Tags: tags, context: &context{}, sampleRate: 1.0, queue: make(chan *outgoingPacket, MaxQueueBuffer), } client.SetDSN(os.Getenv("SENTRY_DSN")) client.SetRelease(os.Getenv("SENTRY_RELEASE")) client.SetEnvironment(os.Getenv("SENTRY_ENVIRONMENT")) return client } // New constructs a new Sentry client instance func New(dsn string) (*Client, error) { client := newClient(nil) return client, client.SetDSN(dsn) } // NewWithTags constructs a new Sentry client instance with default tags. func NewWithTags(dsn string, tags map[string]string) (*Client, error) { client := newClient(tags) return client, client.SetDSN(dsn) } // NewClient constructs a Sentry client and spawns a background goroutine to // handle packets sent by Client.Report. // // Deprecated: use New and NewWithTags instead func NewClient(dsn string, tags map[string]string) (*Client, error) { client := newClient(tags) return client, client.SetDSN(dsn) } // Client encapsulates a connection to a Sentry server. It must be initialized // by calling NewClient. Modification of fields concurrently with Send or after // calling Report for the first time is not thread-safe. type Client struct { Tags map[string]string Transport Transport // DropHandler is called when a packet is dropped because the buffer is full. DropHandler func(*Packet) // Context that will get appending to all packets context *context mu sync.RWMutex url string projectID string authHeader string release string environment string sampleRate float32 // default logger name (leave empty for 'root') defaultLoggerName string includePaths []string ignoreErrorsRegexp *regexp.Regexp queue chan *outgoingPacket // A WaitGroup to keep track of all currently in-progress captures // This is intended to be used with Client.Wait() to assure that // all messages have been transported before exiting the process. wg sync.WaitGroup // A Once to track only starting up the background worker once start sync.Once } // Initialize a default *Client instance var DefaultClient = newClient(nil) func (c *Client) SetIgnoreErrors(errs []string) error { joinedRegexp := strings.Join(errs, "|") r, err := regexp.Compile(joinedRegexp) if err != nil { return fmt.Errorf("failed to compile regexp %q for %q: %v", joinedRegexp, errs, err) } c.mu.Lock() c.ignoreErrorsRegexp = r c.mu.Unlock() return nil } func (c *Client) shouldExcludeErr(errStr string) bool { c.mu.RLock() defer c.mu.RUnlock() return c.ignoreErrorsRegexp != nil && c.ignoreErrorsRegexp.MatchString(errStr) } func SetIgnoreErrors(errs ...string) error { return DefaultClient.SetIgnoreErrors(errs) } // SetDSN updates a client with a new DSN. It safe to call after and // concurrently with calls to Report and Send. func (client *Client) SetDSN(dsn string) error { if dsn == "" { return nil } client.mu.Lock() defer client.mu.Unlock() uri, err := url.Parse(dsn) if err != nil { return err } if uri.User == nil { return ErrMissingUser } publicKey := uri.User.Username() secretKey, hasSecretKey := uri.User.Password() uri.User = nil if idx := strings.LastIndex(uri.Path, "/"); idx != -1 { client.projectID = uri.Path[idx+1:] uri.Path = uri.Path[:idx+1] + "api/" + client.projectID + "/store/" } if client.projectID == "" { return ErrMissingProjectID } client.url = uri.String() if hasSecretKey { client.authHeader = fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s, sentry_secret=%s", publicKey, secretKey) } else { client.authHeader = fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s", publicKey) } return nil } // Sets the DSN for the default *Client instance func SetDSN(dsn string) error { return DefaultClient.SetDSN(dsn) } // SetRelease sets the "release" tag. func (client *Client) SetRelease(release string) { client.mu.Lock() defer client.mu.Unlock() client.release = release } // SetEnvironment sets the "environment" tag. func (client *Client) SetEnvironment(environment string) { client.mu.Lock() defer client.mu.Unlock() client.environment = environment } // SetDefaultLoggerName sets the default logger name. func (client *Client) SetDefaultLoggerName(name string) { client.mu.Lock() defer client.mu.Unlock() client.defaultLoggerName = name } // SetSampleRate sets how much sampling we want on client side func (client *Client) SetSampleRate(rate float32) error { client.mu.Lock() defer client.mu.Unlock() if rate < 0 || rate > 1 { return ErrInvalidSampleRate } client.sampleRate = rate return nil } // SetRelease sets the "release" tag on the default *Client func SetRelease(release string) { DefaultClient.SetRelease(release) } // SetEnvironment sets the "environment" tag on the default *Client func SetEnvironment(environment string) { DefaultClient.SetEnvironment(environment) } // SetDefaultLoggerName sets the "defaultLoggerName" on the default *Client func SetDefaultLoggerName(name string) { DefaultClient.SetDefaultLoggerName(name) } // SetSampleRate sets the "sample rate" on the degault *Client func SetSampleRate(rate float32) error { return DefaultClient.SetSampleRate(rate) } func (client *Client) worker() { for outgoingPacket := range client.queue { client.mu.RLock() url, authHeader := client.url, client.authHeader client.mu.RUnlock() outgoingPacket.ch <- client.Transport.Send(url, authHeader, outgoingPacket.packet) client.wg.Done() } } // Capture asynchronously delivers a packet to the Sentry server. It is a no-op // when client is nil. A channel is provided if it is important to check for a // send's success. func (client *Client) Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) { ch = make(chan error, 1) if client == nil { // return a chan that always returns nil when the caller receives from it close(ch) return } if client.sampleRate < 1.0 && mrand.Float32() > client.sampleRate { return } if packet == nil { close(ch) return } if client.shouldExcludeErr(packet.Message) { return } // Keep track of all running Captures so that we can wait for them all to finish // *Must* call client.wg.Done() on any path that indicates that an event was // finished being acted upon, whether success or failure client.wg.Add(1) // Merge capture tags and client tags packet.AddTags(captureTags) packet.AddTags(client.Tags) // Initialize any required packet fields client.mu.RLock() packet.AddTags(client.context.tags) projectID := client.projectID release := client.release environment := client.environment defaultLoggerName := client.defaultLoggerName client.mu.RUnlock() // set the global logger name on the packet if we must if packet.Logger == "" && defaultLoggerName != "" { packet.Logger = defaultLoggerName } err := packet.Init(projectID) if err != nil { ch <- err client.wg.Done() return } if packet.Release == "" { packet.Release = release } if packet.Environment == "" { packet.Environment = environment } outgoingPacket := &outgoingPacket{packet, ch} // Lazily start background worker until we // do our first write into the queue. client.start.Do(func() { go client.worker() }) select { case client.queue <- outgoingPacket: default: // Send would block, drop the packet if client.DropHandler != nil { client.DropHandler(packet) } ch <- ErrPacketDropped client.wg.Done() } return packet.EventID, ch } // Capture asynchronously delivers a packet to the Sentry server with the default *Client. // It is a no-op when client is nil. A channel is provided if it is important to check for a // send's success. func Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) { return DefaultClient.Capture(packet, captureTags) } // CaptureMessage formats and delivers a string message to the Sentry server. func (client *Client) CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string { if client == nil { return "" } if client.shouldExcludeErr(message) { return "" } packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...) eventID, _ := client.Capture(packet, tags) return eventID } // CaptureMessage formats and delivers a string message to the Sentry server with the default *Client func CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string { return DefaultClient.CaptureMessage(message, tags, interfaces...) } // CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent. func (client *Client) CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string { if client == nil { return "" } if client.shouldExcludeErr(message) { return "" } packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...) eventID, ch := client.Capture(packet, tags) if eventID != "" { <-ch } return eventID } // CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent. func CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string { return DefaultClient.CaptureMessageAndWait(message, tags, interfaces...) } // CaptureErrors formats and delivers an error to the Sentry server. // Adds a stacktrace to the packet, excluding the call to this method. func (client *Client) CaptureError(err error, tags map[string]string, interfaces ...Interface) string { if client == nil { return "" } if err == nil { return "" } if client.shouldExcludeErr(err.Error()) { return "" } extra := extractExtra(err) cause := pkgErrors.Cause(err) packet := NewPacketWithExtra(err.Error(), extra, append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...) eventID, _ := client.Capture(packet, tags) return eventID } // CaptureErrors formats and delivers an error to the Sentry server using the default *Client. // Adds a stacktrace to the packet, excluding the call to this method. func CaptureError(err error, tags map[string]string, interfaces ...Interface) string { return DefaultClient.CaptureError(err, tags, interfaces...) } // CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent func (client *Client) CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string { if client == nil { return "" } if client.shouldExcludeErr(err.Error()) { return "" } extra := extractExtra(err) cause := pkgErrors.Cause(err) packet := NewPacketWithExtra(err.Error(), extra, append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...) eventID, ch := client.Capture(packet, tags) if eventID != "" { <-ch } return eventID } // CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent func CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string { return DefaultClient.CaptureErrorAndWait(err, tags, interfaces...) } // CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs. // If an error is captured, both the error and the reported Sentry error ID are returned. func (client *Client) CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) { // Note: This doesn't need to check for client, because we still want to go through the defer/recover path // Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the // *Packet just to be thrown away, this should not be the normal case. Could be refactored to // be completely noop though if we cared. defer func() { var packet *Packet err = recover() switch rval := err.(type) { case nil: return case error: if client.shouldExcludeErr(rval.Error()) { return } packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...) default: rvalStr := fmt.Sprint(rval) if client.shouldExcludeErr(rvalStr) { return } packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...) } errorID, _ = client.Capture(packet, tags) }() f() return } // CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs. // If an error is captured, both the error and the reported Sentry error ID are returned. func CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) { return DefaultClient.CapturePanic(f, tags, interfaces...) } // CapturePanicAndWait is identical to CaptureError, except it blocks and assures that the event was sent func (client *Client) CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) { // Note: This doesn't need to check for client, because we still want to go through the defer/recover path // Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the // *Packet just to be thrown away, this should not be the normal case. Could be refactored to // be completely noop though if we cared. defer func() { var packet *Packet err = recover() switch rval := err.(type) { case nil: return case error: if client.shouldExcludeErr(rval.Error()) { return } packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...) default: rvalStr := fmt.Sprint(rval) if client.shouldExcludeErr(rvalStr) { return } packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...) } var ch chan error errorID, ch = client.Capture(packet, tags) if errorID != "" { <-ch } }() f() return } // CapturePanicAndWait is identical to CaptureError, except it blocks and assures that the event was sent func CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) { return DefaultClient.CapturePanicAndWait(f, tags, interfaces...) } func (client *Client) Close() { close(client.queue) } func Close() { DefaultClient.Close() } // Wait blocks and waits for all events to finish being sent to Sentry server func (client *Client) Wait() { client.wg.Wait() } // Wait blocks and waits for all events to finish being sent to Sentry server func Wait() { DefaultClient.Wait() } func (client *Client) URL() string { client.mu.RLock() defer client.mu.RUnlock() return client.url } func URL() string { return DefaultClient.URL() } func (client *Client) ProjectID() string { client.mu.RLock() defer client.mu.RUnlock() return client.projectID } func ProjectID() string { return DefaultClient.ProjectID() } func (client *Client) Release() string { client.mu.RLock() defer client.mu.RUnlock() return client.release } func Release() string { return DefaultClient.Release() } func IncludePaths() []string { return DefaultClient.IncludePaths() } func (client *Client) IncludePaths() []string { client.mu.RLock() defer client.mu.RUnlock() return client.includePaths } func SetIncludePaths(p []string) { DefaultClient.SetIncludePaths(p) } func (client *Client) SetIncludePaths(p []string) { client.mu.Lock() defer client.mu.Unlock() client.includePaths = p } func (c *Client) SetUserContext(u *User) { c.mu.Lock() defer c.mu.Unlock() c.context.setUser(u) } func (c *Client) SetHttpContext(h *Http) { c.mu.Lock() defer c.mu.Unlock() c.context.setHttp(h) } func (c *Client) SetTagsContext(t map[string]string) { c.mu.Lock() defer c.mu.Unlock() c.context.setTags(t) } func (c *Client) ClearContext() { c.mu.Lock() defer c.mu.Unlock() c.context.clear() } func SetUserContext(u *User) { DefaultClient.SetUserContext(u) } func SetHttpContext(h *Http) { DefaultClient.SetHttpContext(h) } func SetTagsContext(t map[string]string) { DefaultClient.SetTagsContext(t) } func ClearContext() { DefaultClient.ClearContext() } // HTTPTransport is the default transport, delivering packets to Sentry via the // HTTP API. type HTTPTransport struct { *http.Client } func (t *HTTPTransport) Send(url, authHeader string, packet *Packet) error { if url == "" { return nil } body, contentType, err := serializedPacket(packet) if err != nil { return fmt.Errorf("error serializing packet: %v", err) } req, err := http.NewRequest("POST", url, body) if err != nil { return fmt.Errorf("can't create new request: %v", err) } req.Header.Set("X-Sentry-Auth", authHeader) req.Header.Set("User-Agent", userAgent) req.Header.Set("Content-Type", contentType) res, err := t.Do(req) if err != nil { return err } io.Copy(ioutil.Discard, res.Body) res.Body.Close() if res.StatusCode != 200 { return fmt.Errorf("raven: got http status %d - x-sentry-error: %s", res.StatusCode, res.Header.Get("X-Sentry-Error")) } return nil } func serializedPacket(packet *Packet) (io.Reader, string, error) { packetJSON, err := packet.JSON() if err != nil { return nil, "", fmt.Errorf("error marshaling packet %+v to JSON: %v", packet, err) } // Only deflate/base64 the packet if it is bigger than 1KB, as there is // overhead. if len(packetJSON) > 1000 { buf := &bytes.Buffer{} b64 := base64.NewEncoder(base64.StdEncoding, buf) deflate, _ := zlib.NewWriterLevel(b64, zlib.BestCompression) deflate.Write(packetJSON) deflate.Close() b64.Close() return buf, "application/octet-stream", nil } return bytes.NewReader(packetJSON), "application/json", nil } var hostname string func init() { hostname, _ = os.Hostname() } raven-go-0.2.0/client_test.go000066400000000000000000000212661337761117200161120ustar00rootroot00000000000000package raven import ( "encoding/json" "reflect" "testing" "time" ) type testInterface struct{} func (t *testInterface) Class() string { return "sentry.interfaces.Test" } func (t *testInterface) Culprit() string { return "codez" } func TestShouldExcludeErr(t *testing.T) { regexpStrs := []string{"ERR_TIMEOUT", "should.exclude", "(?i)^big$"} client := &Client{ Transport: newTransport(), Tags: nil, context: &context{}, queue: make(chan *outgoingPacket, MaxQueueBuffer), } if err := client.SetIgnoreErrors(regexpStrs); err != nil { t.Fatalf("invalid regexps %v: %v", regexpStrs, err) } testCases := []string{ "there was a ERR_TIMEOUT in handlers.go", "do not log should.exclude at all", "BIG", } for _, tc := range testCases { if !client.shouldExcludeErr(tc) { t.Fatalf("failed to exclude err %q with regexps %v", tc, regexpStrs) } } } func TestPacketJSON(t *testing.T) { packet := &Packet{ Project: "1", EventID: "2", Platform: "linux", Culprit: "caused_by", ServerName: "host1", Release: "721e41770371db95eee98ca2707686226b993eda", Environment: "production", Message: "test", Timestamp: Timestamp(time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC)), Level: ERROR, Logger: "com.getsentry.raven-go.logger-test-packet-json", Tags: []Tag{Tag{"foo", "bar"}}, Modules: map[string]string{"foo": "bar"}, Fingerprint: []string{"{{ default }}", "a-custom-fingerprint"}, Interfaces: []Interface{&Message{Message: "foo"}}, } packet.AddTags(map[string]string{"foo": "foo"}) packet.AddTags(map[string]string{"baz": "buzz"}) 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"}}` j, err := packet.JSON() if err != nil { t.Fatalf("JSON marshalling should not fail: %v", err) } actual := string(j) if actual != expected { t.Errorf("incorrect json; got %s, want %s", actual, expected) } } func TestPacketJSONNilInterface(t *testing.T) { packet := &Packet{ Project: "1", EventID: "2", Platform: "linux", Culprit: "caused_by", ServerName: "host1", Release: "721e41770371db95eee98ca2707686226b993eda", Environment: "production", Message: "test", Timestamp: Timestamp(time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC)), Level: ERROR, Logger: "com.getsentry.raven-go.logger-test-packet-json", Tags: []Tag{Tag{"foo", "bar"}}, Modules: map[string]string{"foo": "bar"}, Fingerprint: []string{"{{ default }}", "a-custom-fingerprint"}, Interfaces: []Interface{&Message{Message: "foo"}, nil}, } 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"}}` j, err := packet.JSON() if err != nil { t.Fatalf("JSON marshalling should not fail: %v", err) } actual := string(j) if actual != expected { t.Errorf("incorrect json; got %s, want %s", actual, expected) } } func TestPacketInit(t *testing.T) { packet := &Packet{Message: "a", Interfaces: []Interface{&testInterface{}}} packet.Init("foo") if packet.Project != "foo" { t.Error("incorrect Project:", packet.Project) } if packet.Culprit != "codez" { t.Error("incorrect Culprit:", packet.Culprit) } if packet.ServerName == "" { t.Errorf("ServerName should not be empty") } if packet.Level != ERROR { t.Errorf("incorrect Level: got %s, want %s", packet.Level, ERROR) } if packet.Logger != "root" { t.Errorf("incorrect Logger: got %s, want %s", packet.Logger, "root") } if time.Time(packet.Timestamp).IsZero() { t.Error("Timestamp is zero") } if len(packet.EventID) != 32 { t.Error("incorrect EventID:", packet.EventID) } } func TestSetDSN(t *testing.T) { client := &Client{} client.SetDSN("https://u:p@example.com/sentry/1") if client.url != "https://example.com/sentry/api/1/store/" { t.Error("incorrect url:", client.url) } if client.projectID != "1" { t.Error("incorrect projectID:", client.projectID) } if client.authHeader != "Sentry sentry_version=4, sentry_key=u, sentry_secret=p" { t.Error("incorrect authHeader:", client.authHeader) } } func TestNewClient(t *testing.T) { client := newClient(nil) if client.sampleRate != 1.0 { t.Error("invalid default sample rate") } } func TestSetSampleRate(t *testing.T) { client := &Client{} err := client.SetSampleRate(0.2) if err != nil { t.Error("invalid sample rate") } if client.sampleRate != 0.2 { t.Error("incorrect sample rate: ", client.sampleRate) } } func TestSetSampleRateInvalid(t *testing.T) { client := &Client{} err := client.SetSampleRate(-1.0) if err != ErrInvalidSampleRate { t.Error("invalid sample rate should return ErrInvalidSampleRate") } } func TestUnmarshalTag(t *testing.T) { actual := new(Tag) if err := json.Unmarshal([]byte(`["foo","bar"]`), actual); err != nil { t.Fatal("unable to decode JSON:", err) } expected := &Tag{Key: "foo", Value: "bar"} if !reflect.DeepEqual(actual, expected) { t.Errorf("incorrect Tag: wanted '%+v' and got '%+v'", expected, actual) } } func TestUnmarshalTags(t *testing.T) { tests := []struct { Input string Expected Tags }{ { `{"foo":"bar"}`, Tags{Tag{Key: "foo", Value: "bar"}}, }, { `[["foo","bar"],["bar","baz"]]`, Tags{Tag{Key: "foo", Value: "bar"}, Tag{Key: "bar", Value: "baz"}}, }, } for _, test := range tests { var actual Tags if err := json.Unmarshal([]byte(test.Input), &actual); err != nil { t.Fatal("unable to decode JSON:", err) } if !reflect.DeepEqual(actual, test.Expected) { t.Errorf("incorrect Tags: wanted '%+v' and got '%+v'", test.Expected, actual) } } } func TestMarshalTimestamp(t *testing.T) { timestamp := Timestamp(time.Date(2000, 01, 02, 03, 04, 05, 0, time.UTC)) expected := `"2000-01-02T03:04:05.00"` actual, err := json.Marshal(timestamp) if err != nil { t.Error(err) } if string(actual) != expected { t.Errorf("incorrect string; got %s, want %s", actual, expected) } } func TestUnmarshalTimestamp(t *testing.T) { timestamp := `"2000-01-02T03:04:05.00"` expected := Timestamp(time.Date(2000, 01, 02, 03, 04, 05, 0, time.UTC)) var actual Timestamp err := json.Unmarshal([]byte(timestamp), &actual) if err != nil { t.Error(err) } if actual != expected { 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")) } } func TestNilClient(t *testing.T) { var client *Client = nil eventID, ch := client.Capture(nil, nil) if eventID != "" { t.Error("expected empty eventID:", eventID) } // wait on ch: no send should succeed immediately err := <-ch if err != nil { t.Error("expected nil err:", err) } } func TestCaptureNil(t *testing.T) { var client *Client = DefaultClient eventID, ch := client.Capture(nil, nil) if eventID != "" { t.Error("expected empty eventID:", eventID) } // wait on ch: no send should succeed immediately err := <-ch if err != nil { t.Error("expected nil err:", err) } } func TestCaptureNilError(t *testing.T) { var client *Client = DefaultClient eventID := client.CaptureError(nil, nil) if eventID != "" { t.Error("expected empty eventID:", eventID) } } func TestNewPacketWithExtraSetsDefault(t *testing.T) { testCases := []struct { Extra Extra Expected Extra }{ // Defaults should be set when nil is passed { Extra: nil, Expected: setExtraDefaults(Extra{}), }, // Defaults should be set when empty is passed { Extra: Extra{}, Expected: setExtraDefaults(Extra{}), }, // Packet should always override default keys { Extra: Extra{ "runtime.Version": "notagoversion", }, Expected: setExtraDefaults(Extra{}), }, // Packet should include our extra info { Extra: Extra{ "extra.extra": "extra", }, Expected: setExtraDefaults(Extra{ "extra.extra": "extra", }), }, } for i, test := range testCases { packet := NewPacketWithExtra("packet", test.Extra) if !reflect.DeepEqual(packet.Extra, test.Expected) { t.Errorf("Case [%d]: Expected packet: %+v, got: %+v", i, test.Expected, packet.Extra) } } } raven-go-0.2.0/errors.go000066400000000000000000000022641337761117200151060ustar00rootroot00000000000000package raven type causer interface { Cause() error } type errWrappedWithExtra struct { err error extraInfo map[string]interface{} } func (ewx *errWrappedWithExtra) Error() string { return ewx.err.Error() } func (ewx *errWrappedWithExtra) Cause() error { return ewx.err } func (ewx *errWrappedWithExtra) ExtraInfo() Extra { return ewx.extraInfo } // Adds extra data to an error before reporting to Sentry func WrapWithExtra(err error, extraInfo map[string]interface{}) error { return &errWrappedWithExtra{ err: err, extraInfo: extraInfo, } } type ErrWithExtra interface { Error() string Cause() error ExtraInfo() Extra } // Iteratively fetches all the Extra data added to an error, // and it's underlying errors. Extra data defined first is // respected, and is not overridden when extracting. func extractExtra(err error) Extra { extra := Extra{} currentErr := err for currentErr != nil { if errWithExtra, ok := currentErr.(ErrWithExtra); ok { for k, v := range errWithExtra.ExtraInfo() { extra[k] = v } } if errWithCause, ok := currentErr.(causer); ok { currentErr = errWithCause.Cause() } else { currentErr = nil } } return extra } raven-go-0.2.0/errors_test.go000066400000000000000000000067671337761117200161610ustar00rootroot00000000000000package raven import ( "fmt" "reflect" "testing" pkgErrors "github.com/pkg/errors" ) func TestWrapWithExtraGeneratesProperErrWithExtra(t *testing.T) { errMsg := "This is bad" baseErr := fmt.Errorf(errMsg) extraInfo := map[string]interface{}{ "string": "string", "int": 1, "float": 1.001, "bool": false, } testErr := WrapWithExtra(baseErr, extraInfo) wrapped, ok := testErr.(ErrWithExtra) if !ok { t.Errorf("Wrapped error does not conform to expected protocol.") } if !reflect.DeepEqual(wrapped.Cause(), baseErr) { t.Errorf("Failed to unwrap error, got %+v, expected %+v", wrapped.Cause(), baseErr) } returnedExtra := wrapped.ExtraInfo() for expectedKey, expectedVal := range extraInfo { val, ok := returnedExtra[expectedKey] if !ok { t.Errorf("Extra data missing key: %s", expectedKey) } if val != expectedVal { t.Errorf("Extra data [%s]: Got: %+v, expected: %+v", expectedKey, val, expectedVal) } } if wrapped.Error() != errMsg { t.Errorf("Wrong error message, got: %q, expected: %q", wrapped.Error(), errMsg) } } func TestWrapWithExtraGeneratesCausableError(t *testing.T) { baseErr := fmt.Errorf("this is bad") testErr := WrapWithExtra(baseErr, nil) cause := pkgErrors.Cause(testErr) if !reflect.DeepEqual(cause, baseErr) { t.Errorf("Failed to unwrap error, got %+v, expected %+v", cause, baseErr) } } func TestExtractErrorPullsExtraData(t *testing.T) { extraInfo := map[string]interface{}{ "string": "string", "int": 1, "float": 1.001, "bool": false, } emptyInfo := map[string]interface{}{} testCases := []struct { Error error Expected map[string]interface{} }{ // Unwrapped error shouldn't include anything { Error: fmt.Errorf("This is bad"), Expected: emptyInfo, }, // Wrapped error with nil map should extract as empty info { Error: WrapWithExtra(fmt.Errorf("This is bad"), nil), Expected: emptyInfo, }, // Wrapped error with empty map should extract as empty info { Error: WrapWithExtra(fmt.Errorf("This is bad"), emptyInfo), Expected: emptyInfo, }, // Wrapped error with extra info should extract with all data { Error: WrapWithExtra(fmt.Errorf("This is bad"), extraInfo), Expected: extraInfo, }, // Nested wrapped error should extract all the info { Error: WrapWithExtra( WrapWithExtra(fmt.Errorf("This is bad"), map[string]interface{}{ "inner": "123", }), map[string]interface{}{ "outer": "456", }, ), Expected: map[string]interface{}{ "inner": "123", "outer": "456", }, }, // Futher wrapping of errors shouldn't allow for value override { Error: WrapWithExtra( WrapWithExtra(fmt.Errorf("This is bad"), map[string]interface{}{ "dontoverride": "123", }), map[string]interface{}{ "dontoverride": "456", }, ), Expected: map[string]interface{}{ "dontoverride": "123", }, }, } for i, test := range testCases { extracted := extractExtra(test.Error) if len(test.Expected) != len(extracted) { t.Errorf( "Case [%d]: Mismatched amount of data between provided and extracted extra. Got: %+v Expected: %+v", i, extracted, test.Expected, ) } for expectedKey, expectedVal := range test.Expected { val, ok := extracted[expectedKey] if !ok { t.Errorf("Case [%d]: Extra data missing key: %s", i, expectedKey) } if val != expectedVal { t.Errorf("Case [%d]: Wrong extra data for %q. Got: %+v, expected: %+v", i, expectedKey, val, expectedVal) } } } } raven-go-0.2.0/example/000077500000000000000000000000001337761117200146725ustar00rootroot00000000000000raven-go-0.2.0/example/example.go000066400000000000000000000024061337761117200166560ustar00rootroot00000000000000package main import ( "errors" "fmt" "github.com/getsentry/raven-go" "log" "net/http" "os" ) func trace() *raven.Stacktrace { return raven.NewStacktrace(0, 2, nil) } func main() { client, err := raven.NewWithTags(os.Args[1], map[string]string{"foo": "bar"}) if err != nil { log.Fatal(err) } httpReq, _ := http.NewRequest("GET", "http://example.com/foo?bar=true", nil) httpReq.RemoteAddr = "127.0.0.1:80" httpReq.Header = http.Header{"Content-Type": {"text/html"}, "Content-Length": {"42"}} packet := &raven.Packet{Message: "Test report", Interfaces: []raven.Interface{raven.NewException(errors.New("example"), trace()), raven.NewHttp(httpReq)}} _, ch := client.Capture(packet, nil) if err = <-ch; err != nil { log.Fatal(err) } log.Print("sent packet successfully") } // CheckError sends error report to sentry and records event id and error name to the logs func CheckError(err error, r *http.Request) { client, err := raven.NewWithTags(os.Args[1], map[string]string{"foo": "bar"}) if err != nil { log.Fatal(err) } packet := raven.NewPacket(err.Error(), raven.NewException(err, trace()), raven.NewHttp(r)) eventID, _ := client.Capture(packet, nil) message := fmt.Sprintf("Error event with id \"%s\" - %s", eventID, err.Error()) log.Println(message) } raven-go-0.2.0/examples_test.go000066400000000000000000000012061337761117200164420ustar00rootroot00000000000000package raven import ( "fmt" "log" "net/http" ) func Example() { // ... i.e. raisedErr is incoming error var raisedErr error // sentry DSN generated by Sentry server var sentryDSN string // r is a request performed when error occured var r *http.Request client, err := New(sentryDSN) if err != nil { log.Fatal(err) } trace := NewStacktrace(0, 2, nil) packet := NewPacket(raisedErr.Error(), NewException(raisedErr, trace), NewHttp(r)) eventID, ch := client.Capture(packet, nil) if err = <-ch; err != nil { log.Fatal(err) } message := fmt.Sprintf("Captured error with id %s: %q", eventID, raisedErr) log.Println(message) } raven-go-0.2.0/exception.go000066400000000000000000000021641337761117200155670ustar00rootroot00000000000000package raven import ( "reflect" "regexp" ) var errorMsgPattern = regexp.MustCompile(`\A(\w+): (.+)\z`) func NewException(err error, stacktrace *Stacktrace) *Exception { msg := err.Error() ex := &Exception{ Stacktrace: stacktrace, Value: msg, Type: reflect.TypeOf(err).String(), } if m := errorMsgPattern.FindStringSubmatch(msg); m != nil { ex.Module, ex.Value = m[1], m[2] } return ex } // https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces type Exception struct { // Required Value string `json:"value"` // Optional Type string `json:"type,omitempty"` Module string `json:"module,omitempty"` Stacktrace *Stacktrace `json:"stacktrace,omitempty"` } func (e *Exception) Class() string { return "exception" } func (e *Exception) Culprit() string { if e.Stacktrace == nil { return "" } return e.Stacktrace.Culprit() } // Exceptions allows for chained errors // https://docs.sentry.io/clientdev/interfaces/exception/ type Exceptions struct { // Required Values []*Exception `json:"values"` } func (es Exceptions) Class() string { return "exception" } raven-go-0.2.0/exception_test.go000066400000000000000000000020331337761117200166210ustar00rootroot00000000000000package raven import ( "encoding/json" "errors" "testing" ) var newExceptionTests = []struct { err error Exception }{ {errors.New("foobar"), Exception{Value: "foobar", Type: "*errors.errorString"}}, {errors.New("bar: foobar"), Exception{Value: "foobar", Type: "*errors.errorString", Module: "bar"}}, } func TestNewException(t *testing.T) { for _, test := range newExceptionTests { actual := NewException(test.err, nil) if actual.Value != test.Value { t.Errorf("incorrect Value: got %s, want %s", actual.Value, test.Value) } if actual.Type != test.Type { t.Errorf("incorrect Type: got %s, want %s", actual.Type, test.Type) } if actual.Module != test.Module { t.Errorf("incorrect Module: got %s, want %s", actual.Module, test.Module) } } } func TestNewException_JSON(t *testing.T) { expected := `{"value":"foobar","type":"*errors.errorString"}` e := NewException(errors.New("foobar"), nil) b, _ := json.Marshal(e) if string(b) != expected { t.Errorf("incorrect JSON: got %s, want %s", string(b), expected) } } raven-go-0.2.0/http.go000066400000000000000000000052601337761117200145500ustar00rootroot00000000000000package raven import ( "errors" "fmt" "net" "net/http" "net/url" "runtime/debug" "strings" ) func NewHttp(req *http.Request) *Http { proto := "http" if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" { proto = "https" } h := &Http{ Method: req.Method, Cookies: req.Header.Get("Cookie"), Query: sanitizeQuery(req.URL.Query()).Encode(), URL: proto + "://" + req.Host + req.URL.Path, Headers: make(map[string]string, len(req.Header)), } if addr, port, err := net.SplitHostPort(req.RemoteAddr); err == nil { h.Env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port} } for k, v := range req.Header { h.Headers[k] = strings.Join(v, ",") } h.Headers["Host"] = req.Host return h } var querySecretFields = []string{"password", "passphrase", "passwd", "secret"} func sanitizeQuery(query url.Values) url.Values { for _, keyword := range querySecretFields { for field := range query { if strings.Contains(field, keyword) { query[field] = []string{"********"} } } } return query } // https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces type Http struct { // Required URL string `json:"url"` Method string `json:"method"` Query string `json:"query_string,omitempty"` // Optional Cookies string `json:"cookies,omitempty"` Headers map[string]string `json:"headers,omitempty"` Env map[string]string `json:"env,omitempty"` // Must be either a string or map[string]string Data interface{} `json:"data,omitempty"` } func (h *Http) Class() string { return "request" } // Recovery handler to wrap the stdlib net/http Mux. // Example: // http.HandleFunc("/", raven.RecoveryHandler(func(w http.ResponseWriter, r *http.Request) { // ... // })) func RecoveryHandler(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { return Recoverer(http.HandlerFunc(handler)).ServeHTTP } // Recovery handler to wrap the stdlib net/http Mux. // Example: // mux := http.NewServeMux // ... // http.Handle("/", raven.Recoverer(mux)) func Recoverer(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rval := recover(); rval != nil { debug.PrintStack() rvalStr := fmt.Sprint(rval) var packet *Packet if err, ok := rval.(error); ok { packet = NewPacket(rvalStr, NewException(errors.New(rvalStr), GetOrNewStacktrace(err, 2, 3, nil)), NewHttp(r)) } else { packet = NewPacket(rvalStr, NewException(errors.New(rvalStr), NewStacktrace(2, 3, nil)), NewHttp(r)) } Capture(packet, nil) w.WriteHeader(http.StatusInternalServerError) } }() handler.ServeHTTP(w, r) }) } raven-go-0.2.0/http_test.go000066400000000000000000000067341337761117200156160ustar00rootroot00000000000000package raven import ( "net/http" "net/url" "reflect" "testing" ) type testcase struct { request *http.Request *Http } func newBaseRequest() *http.Request { u, _ := url.Parse("http://example.com/") header := make(http.Header) header.Add("Foo", "bar") req := &http.Request{ Method: "GET", URL: u, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: header, Host: u.Host, RemoteAddr: "127.0.0.1:8000", } return req } func newBaseHttp() *Http { h := &Http{ Method: "GET", Cookies: "", Query: "", URL: "http://example.com/", Headers: map[string]string{"Foo": "bar"}, Env: map[string]string{"REMOTE_ADDR": "127.0.0.1", "REMOTE_PORT": "8000"}, } h.Headers["Host"] = "example.com" return h } func NewRequest() testcase { return testcase{newBaseRequest(), newBaseHttp()} } func NewRequestIPV6() testcase { req := newBaseRequest() req.RemoteAddr = "[:1]:8000" h := newBaseHttp() h.Env = map[string]string{"REMOTE_ADDR": ":1", "REMOTE_PORT": "8000"} return testcase{req, h} } func NewRequestMultipleHeaders() testcase { req := newBaseRequest() req.Header.Add("Foo", "baz") h := newBaseHttp() h.Headers["Foo"] = "bar,baz" return testcase{req, h} } func NewSecureRequest() testcase { req := newBaseRequest() req.Header.Add("X-Forwarded-Proto", "https") h := newBaseHttp() h.URL = "https://example.com/" h.Headers["X-Forwarded-Proto"] = "https" return testcase{req, h} } func NewCookiesRequest() testcase { val := "foo=bar; bar=baz" req := newBaseRequest() req.Header.Add("Cookie", val) h := newBaseHttp() h.Cookies = val h.Headers["Cookie"] = val return testcase{req, h} } var newHttpTests = []testcase{ NewRequest(), NewRequestIPV6(), NewRequestMultipleHeaders(), NewSecureRequest(), NewCookiesRequest(), } func TestNewHttp(t *testing.T) { for _, test := range newHttpTests { actual := NewHttp(test.request) if actual.Method != test.Method { t.Errorf("incorrect Method: got %s, want %s", actual.Method, test.Method) } if actual.Cookies != test.Cookies { t.Errorf("incorrect Cookies: got %s, want %s", actual.Cookies, test.Cookies) } if actual.Query != test.Query { t.Errorf("incorrect Query: got %s, want %s", actual.Query, test.Query) } if actual.URL != test.URL { t.Errorf("incorrect URL: got %s, want %s", actual.URL, test.URL) } if !reflect.DeepEqual(actual.Headers, test.Headers) { t.Errorf("incorrect Headers: got %+v, want %+v", actual.Headers, test.Headers) } if !reflect.DeepEqual(actual.Env, test.Env) { t.Errorf("incorrect Env: got %+v, want %+v", actual.Env, test.Env) } if !reflect.DeepEqual(actual.Data, test.Data) { t.Errorf("incorrect Data: got %+v, want %+v", actual.Data, test.Data) } } } var sanitizeQueryTests = []struct { input, output string }{ {"foo=bar", "foo=bar"}, {"password=foo", "password=********"}, {"passphrase=foo", "passphrase=********"}, {"passwd=foo", "passwd=********"}, {"secret=foo", "secret=********"}, {"secretstuff=foo", "secretstuff=********"}, {"foo=bar&secret=foo", "foo=bar&secret=********"}, {"secret=foo&secret=bar", "secret=********"}, } func parseQuery(q string) url.Values { r, _ := url.ParseQuery(q) return r } func TestSanitizeQuery(t *testing.T) { for _, test := range sanitizeQueryTests { actual := sanitizeQuery(parseQuery(test.input)) expected := parseQuery(test.output) if !reflect.DeepEqual(actual, expected) { t.Errorf("incorrect sanitization: got %+v, want %+v", actual, expected) } } } raven-go-0.2.0/interfaces.go000066400000000000000000000024771337761117200157230ustar00rootroot00000000000000package raven // https://docs.getsentry.com/hosted/clientdev/interfaces/#message-interface type Message struct { // Required Message string `json:"message"` // Optional Params []interface{} `json:"params,omitempty"` } func (m *Message) Class() string { return "logentry" } // https://docs.getsentry.com/hosted/clientdev/interfaces/#template-interface type Template struct { // Required Filename string `json:"filename"` Lineno int `json:"lineno"` ContextLine string `json:"context_line"` // Optional PreContext []string `json:"pre_context,omitempty"` PostContext []string `json:"post_context,omitempty"` AbsolutePath string `json:"abs_path,omitempty"` } func (t *Template) Class() string { return "template" } // https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces type User struct { // All fields are optional ID string `json:"id,omitempty"` Username string `json:"username,omitempty"` Email string `json:"email,omitempty"` IP string `json:"ip_address,omitempty"` } func (h *User) Class() string { return "user" } // https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces type Query struct { // Required Query string `json:"query"` // Optional Engine string `json:"engine,omitempty"` } func (q *Query) Class() string { return "query" } raven-go-0.2.0/runtests.sh000077500000000000000000000001061337761117200154620ustar00rootroot00000000000000#!/bin/bash go test -race ./... go test -cover ./... go test -v ./... raven-go-0.2.0/stacktrace.go000066400000000000000000000171141337761117200157160ustar00rootroot00000000000000// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Some code from the runtime/debug package of the Go standard library. package raven import ( "bytes" "go/build" "io/ioutil" "path/filepath" "runtime" "strings" "sync" "github.com/pkg/errors" ) // https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces type Stacktrace struct { // Required Frames []*StacktraceFrame `json:"frames"` } func (s *Stacktrace) Class() string { return "stacktrace" } func (s *Stacktrace) Culprit() string { for i := len(s.Frames) - 1; i >= 0; i-- { frame := s.Frames[i] if frame.InApp == true && frame.Module != "" && frame.Function != "" { return frame.Module + "." + frame.Function } } return "" } type StacktraceFrame struct { // At least one required Filename string `json:"filename,omitempty"` Function string `json:"function,omitempty"` Module string `json:"module,omitempty"` // Optional Lineno int `json:"lineno,omitempty"` Colno int `json:"colno,omitempty"` AbsolutePath string `json:"abs_path,omitempty"` ContextLine string `json:"context_line,omitempty"` PreContext []string `json:"pre_context,omitempty"` PostContext []string `json:"post_context,omitempty"` InApp bool `json:"in_app"` } // Try to get stacktrace from err as an interface of github.com/pkg/errors, or else NewStacktrace() func GetOrNewStacktrace(err error, skip int, context int, appPackagePrefixes []string) *Stacktrace { stacktracer, errHasStacktrace := err.(interface { StackTrace() errors.StackTrace }) if errHasStacktrace { var frames []*StacktraceFrame for _, f := range stacktracer.StackTrace() { pc := uintptr(f) - 1 fn := runtime.FuncForPC(pc) var fName string var file string var line int if fn != nil { file, line = fn.FileLine(pc) fName = fn.Name() } else { file = "unknown" fName = "unknown" } frame := NewStacktraceFrame(pc, fName, file, line, context, appPackagePrefixes) if frame != nil { frames = append([]*StacktraceFrame{frame}, frames...) } } return &Stacktrace{Frames: frames} } else { return NewStacktrace(skip+1, context, appPackagePrefixes) } } // Intialize and populate a new stacktrace, skipping skip frames. // // context is the number of surrounding lines that should be included for context. // Setting context to 3 would try to get seven lines. Setting context to -1 returns // one line with no surrounding context, and 0 returns no context. // // appPackagePrefixes is a list of prefixes used to check whether a package should // be considered "in app". func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace { var frames []*StacktraceFrame callerPcs := make([]uintptr, 100) numCallers := runtime.Callers(skip+2, callerPcs) // If there are no callers, the entire stacktrace is nil if numCallers == 0 { return nil } callersFrames := runtime.CallersFrames(callerPcs) for { fr, more := callersFrames.Next() if fr.Func != nil { frame := NewStacktraceFrame(fr.PC, fr.Function, fr.File, fr.Line, context, appPackagePrefixes) if frame != nil { frames = append(frames, frame) } } if !more { break } } // If there are no frames, the entire stacktrace is nil if len(frames) == 0 { return nil } // Optimize the path where there's only 1 frame if len(frames) == 1 { return &Stacktrace{frames} } // Sentry wants the frames with the oldest first, so reverse them for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 { frames[i], frames[j] = frames[j], frames[i] } return &Stacktrace{frames} } // Build a single frame using data returned from runtime.Caller. // // context is the number of surrounding lines that should be included for context. // Setting context to 3 would try to get seven lines. Setting context to -1 returns // one line with no surrounding context, and 0 returns no context. // // appPackagePrefixes is a list of prefixes used to check whether a package should // be considered "in app". func NewStacktraceFrame(pc uintptr, fName, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame { frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line, InApp: false} frame.Module, frame.Function = functionName(fName) // `runtime.goexit` is effectively a placeholder that comes from // runtime/asm_amd64.s and is meaningless. if frame.Module == "runtime" && frame.Function == "goexit" { return nil } if frame.Module == "main" { frame.InApp = true } else { for _, prefix := range appPackagePrefixes { if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") { frame.InApp = true } } } if context > 0 { contextLines, lineIdx := sourceCodeLoader.Load(file, line, context) if len(contextLines) > 0 { for i, line := range contextLines { switch { case i < lineIdx: frame.PreContext = append(frame.PreContext, string(line)) case i == lineIdx: frame.ContextLine = string(line) default: frame.PostContext = append(frame.PostContext, string(line)) } } } } else if context == -1 { contextLine, _ := sourceCodeLoader.Load(file, line, 0) if len(contextLine) > 0 { frame.ContextLine = string(contextLine[0]) } } return frame } // Retrieve the name of the package and function containing the PC. func functionName(fName string) (pack string, name string) { name = fName // We get this: // runtime/debug.*T·ptrmethod // and want this: // pack = runtime/debug // name = *T.ptrmethod if idx := strings.LastIndex(name, "."); idx != -1 { pack = name[:idx] name = name[idx+1:] } name = strings.Replace(name, "·", ".", -1) return } type SourceCodeLoader interface { Load(filename string, line, context int) ([][]byte, int) } var sourceCodeLoader SourceCodeLoader = &fsLoader{cache: make(map[string][][]byte)} func SetSourceCodeLoader(loader SourceCodeLoader) { sourceCodeLoader = loader } type fsLoader struct { mu sync.Mutex cache map[string][][]byte } func (fs *fsLoader) Load(filename string, line, context int) ([][]byte, int) { fs.mu.Lock() defer fs.mu.Unlock() lines, ok := fs.cache[filename] if !ok { data, err := ioutil.ReadFile(filename) if err != nil { // cache errors as nil slice: code below handles it correctly // otherwise when missing the source or running as a different user, we try // reading the file on each error which is unnecessary fs.cache[filename] = nil return nil, 0 } lines = bytes.Split(data, []byte{'\n'}) fs.cache[filename] = lines } if lines == nil { // cached error from ReadFile: return no lines return nil, 0 } line-- // stack trace lines are 1-indexed start := line - context var idx int if start < 0 { start = 0 idx = line } else { idx = context } end := line + context + 1 if line >= len(lines) { return nil, 0 } if end > len(lines) { end = len(lines) } return lines[start:end], idx } var trimPaths []string // Try to trim the GOROOT or GOPATH prefix off of a filename func trimPath(filename string) string { for _, prefix := range trimPaths { if trimmed := strings.TrimPrefix(filename, prefix); len(trimmed) < len(filename) { return trimmed } } return filename } func init() { // Collect all source directories, and make sure they // end in a trailing "separator" for _, prefix := range build.Default.SrcDirs() { if prefix[len(prefix)-1] != filepath.Separator { prefix += string(filepath.Separator) } trimPaths = append(trimPaths, prefix) } } raven-go-0.2.0/stacktrace_test.go000066400000000000000000000112061337761117200167510ustar00rootroot00000000000000package raven import ( "fmt" "go/build" "io/ioutil" "os" "path/filepath" "runtime" "strings" "testing" ) func init() { thisFile, thisPackage = derivePackage() functionNameTests = []FunctionNameTest{ {0, thisPackage, "TestFunctionName"}, {1, "testing", "tRunner"}, {2, "runtime", "goexit"}, {100, "", ""}, } } type FunctionNameTest struct { skip int pack string name string } var ( thisFile string thisPackage string functionNameTests []FunctionNameTest ) func TestFunctionName(t *testing.T) { for _, test := range functionNameTests { pc, _, _, _ := runtime.Caller(test.skip) pack, name := functionName(runtime.FuncForPC(pc).Name()) if pack != test.pack { t.Errorf("incorrect package; got %s, want %s", pack, test.pack) } if name != test.name { t.Errorf("incorrect function; got %s, want %s", name, test.name) } } } func TestStacktrace(t *testing.T) { st := trace() if st == nil { t.Error("got nil stacktrace") } if len(st.Frames) == 0 { t.Error("got zero frames") } f := st.Frames[len(st.Frames)-1] if f.Filename != thisFile { t.Errorf("incorrect Filename; got %s, want %s", f.Filename, thisFile) } if !strings.HasSuffix(f.AbsolutePath, thisFile) { t.Error("incorrect AbsolutePath:", f.AbsolutePath) } if f.Function != "trace" { t.Error("incorrect Function:", f.Function) } if f.Module != thisPackage { t.Error("incorrect Module:", f.Module) } if f.Lineno != 97 { t.Error("incorrect Lineno:", f.Lineno) } if f.ContextLine != "\treturn NewStacktrace(0, 2, []string{thisPackage})" { t.Errorf("incorrect ContextLine: %#v", f.ContextLine) } if len(f.PreContext) != 2 || f.PreContext[0] != "// a" || f.PreContext[1] != "func trace() *Stacktrace {" { t.Errorf("incorrect PreContext %#v", f.PreContext) } if len(f.PostContext) != 2 || f.PostContext[0] != "\t// b" || f.PostContext[1] != "}" { t.Errorf("incorrect PostContext %#v", f.PostContext) } _, filename, _, _ := runtime.Caller(0) runningInVendored := strings.Contains(filename, "vendor") if f.InApp != !runningInVendored { t.Error("expected InApp to be true") } if f.InApp && st.Culprit() != fmt.Sprintf("%s.trace", thisPackage) { t.Error("incorrect Culprit:", st.Culprit()) } } // a func trace() *Stacktrace { return NewStacktrace(0, 2, []string{thisPackage}) // b } func derivePackage() (file, pack string) { // Get file name by seeking caller's file name. _, callerFile, _, ok := runtime.Caller(1) if !ok { return } // Trim file name file = callerFile for _, dir := range build.Default.SrcDirs() { dir := dir + string(filepath.Separator) if trimmed := strings.TrimPrefix(callerFile, dir); len(trimmed) < len(file) { file = trimmed } } // Now derive package name dir := filepath.Dir(callerFile) dirPkg, err := build.ImportDir(dir, build.AllowBinary) if err != nil { return } pack = dirPkg.ImportPath return } // TestNewStacktrace_outOfBounds verifies that a context exceeding the number // of lines in a file does not cause a panic. func TestNewStacktrace_outOfBounds(t *testing.T) { st := NewStacktrace(0, 1000000, []string{thisPackage}) f := st.Frames[len(st.Frames)-1] if f.ContextLine != "\tst := NewStacktrace(0, 1000000, []string{thisPackage})" { t.Errorf("incorrect ContextLine: %#v", f.ContextLine) } } func TestNewStacktrace_noFrames(t *testing.T) { st := NewStacktrace(999999999, 0, []string{}) if st != nil { t.Errorf("expected st.Frames to be nil: %v", st) } } func TestFileContext(t *testing.T) { // reset the cache loader := &fsLoader{cache: make(map[string][][]byte)} tempdir, err := ioutil.TempDir("", "") if err != nil { t.Fatal("failed to create temporary directory:", err) } defer os.RemoveAll(tempdir) okPath := filepath.Join(tempdir, "ok") missingPath := filepath.Join(tempdir, "missing") noPermissionPath := filepath.Join(tempdir, "noperms") err = ioutil.WriteFile(okPath, []byte("hello\nworld\n"), 0600) if err != nil { t.Fatal("failed writing file:", err) } err = ioutil.WriteFile(noPermissionPath, []byte("no access\n"), 0000) if err != nil { t.Fatal("failed writing file:", err) } tests := []struct { path string expectedLines int expectedIndex int }{ {okPath, 1, 0}, {missingPath, 0, 0}, {noPermissionPath, 0, 0}, } for i, test := range tests { lines, index := loader.Load(test.path, 1, 0) if !(len(lines) == test.expectedLines && index == test.expectedIndex) { t.Errorf("%d: fileContext(%#v, 1, 0) = %v, %v; expected len()=%d, %d", i, test.path, lines, index, test.expectedLines, test.expectedIndex) } cacheLen := len(loader.cache) if cacheLen != i+1 { t.Errorf("%d: result was not cached; len=%d", i, cacheLen) } } } raven-go-0.2.0/writer.go000066400000000000000000000007411337761117200151040ustar00rootroot00000000000000package raven type Writer struct { Client *Client Level Severity Logger string // Logger name reported to Sentry } // Write formats the byte slice p into a string, and sends a message to // Sentry at the severity level indicated by the Writer w. func (w *Writer) Write(p []byte) (int, error) { message := string(p) packet := NewPacket(message, &Message{message, nil}) packet.Level = w.Level packet.Logger = w.Logger w.Client.Capture(packet, nil) return len(p), nil }