Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Initial tracing implementation #285

Merged
merged 1 commit into from
Dec 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
87 changes: 6 additions & 81 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,88 +65,13 @@ More on this in the [Configuration section of the official Sentry Go SDK documen

## Usage

The SDK must be initialized with a call to `sentry.Init`. The default transport
is asynchronous and thus most programs should call `sentry.Flush` to wait until
buffered events are sent to Sentry right before the program terminates.

Typically, `sentry.Init` is called in the beginning of `func main` and
`sentry.Flush` is [deferred](https://golang.org/ref/spec#Defer_statements) right
after.

> Note that if the program terminates with a call to
> [`os.Exit`](https://golang.org/pkg/os/#Exit), either directly or indirectly
> via another function like `log.Fatal`, deferred functions are not run.
>
> In that case, and if it is important for you to report outstanding events
> before terminating the program, arrange for `sentry.Flush` to be called before
> the program terminates.

Example:

```go
// This is an example program that makes an HTTP request and prints response
// headers. Whenever a request fails, the error is reported to Sentry.
//
// Try it by running:
//
// go run main.go
// go run main.go https://sentry.io
// go run main.go bad-url
//
// To actually report events to Sentry, set the DSN either by editing the
// appropriate line below or setting the environment variable SENTRY_DSN to
// match the DSN of your Sentry project.
package main

import (
"fmt"
"log"
"net/http"
"os"
"time"

"github.com/getsentry/sentry-go"
)

func main() {
if len(os.Args) < 2 {
log.Fatalf("usage: %s URL", os.Args[0])
}

err := sentry.Init(sentry.ClientOptions{
// Either set your DSN here or set the SENTRY_DSN environment variable.
Dsn: "",
// Enable printing of SDK debug messages.
// Useful when getting started or trying to figure something out.
Debug: true,
})
if err != nil {
log.Fatalf("sentry.Init: %s", err)
}
// Flush buffered events before the program terminates.
// Set the timeout to the maximum duration the program can afford to wait.
defer sentry.Flush(2 * time.Second)

resp, err := http.Get(os.Args[1])
if err != nil {
sentry.CaptureException(err)
log.Printf("reported to Sentry: %s", err)
return
}
defer resp.Body.Close()

for header, values := range resp.Header {
for _, value := range values {
fmt.Printf("%s=%s\n", header, value)
}
}
}
```
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this example from the README to keep the document more concise. The example is available in full in the example directory, as well as we have some "how to get started" info both in doc comments for the package and in the official docs.

The SDK supports reporting errors and tracking application performance.

To get started, have a look at one of our [examples](example/):
- [Basic error instrumentation](example/basic/main.go)
- [Error and tracing for HTTP servers](example/http/main.go)

For your convenience, this example is available at
[`example/basic/main.go`](example/basic/main.go).
There are also more examples in the
[example](example) directory.
We also provide a [complete API reference](https://pkg.go.dev/github.com/getsentry/sentry-go).

For more detailed information about how to get the most out of `sentry-go`,
checkout the official documentation:
Expand Down
26 changes: 22 additions & 4 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sentry
import (
"context"
"crypto/x509"
"errors"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -119,6 +120,10 @@ type ClientOptions struct {
// 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the
// empty string.
SampleRate float64
// The sample rate for sampling traces in the range [0.0, 1.0].
TracesSampleRate float64
// Used to customize the sampling of traces, overrides TracesSampleRate.
TracesSampler TracesSampler
// List of regexp strings that will be used to match against event's message
// and if applicable, caught errors type and value.
// If the match is found, then a whole event will be dropped.
Expand Down Expand Up @@ -183,6 +188,10 @@ type Client struct {
// single goroutine) or hub methods (for concurrent programs, for example web
// servers).
func NewClient(options ClientOptions) (*Client, error) {
if options.TracesSampleRate != 0.0 && options.TracesSampler != nil {
return nil, errors.New("TracesSampleRate and TracesSampler are mutually exclusive")
}

if options.Debug {
debugWriter := options.DebugWriter
if debugWriter == nil {
Expand Down Expand Up @@ -231,17 +240,26 @@ func NewClient(options ClientOptions) (*Client, error) {
}

func (client *Client) setupTransport() {
transport := client.options.Transport
opts := client.options
transport := opts.Transport

if transport == nil {
if client.options.Dsn == "" {
if opts.Dsn == "" {
transport = new(noopTransport)
} else {
transport = NewHTTPTransport()
httpTransport := NewHTTPTransport()
// When tracing is enabled, use larger buffer to
// accommodate more concurrent events.
// TODO(tracing): consider using separate buffers per
// event type.
if opts.TracesSampleRate != 0 || opts.TracesSampler != nil {
httpTransport.BufferSize = 1000
}
transport = httpTransport
}
}

transport.Configure(client.options)
transport.Configure(opts)
client.Transport = transport
}

Expand Down
25 changes: 25 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/*
Package sentry is the official Sentry SDK for Go.

Use it to report errors and track application performance through distributed
tracing.

For more information about Sentry and SDK features please have a look at the
documentation site https://docs.sentry.io/platforms/go/.

Expand All @@ -17,6 +20,28 @@ Sentry project. This step is accomplished through a call to sentry.Init.
A more detailed yet simple example is available at
https://github.com/getsentry/sentry-go/blob/master/example/basic/main.go.

Error Reporting

The Capture* functions report messages and errors to Sentry.

sentry.CaptureMessage(...)
sentry.CaptureException(...)
sentry.CaptureEvent(...)

Use similarly named functions in the Hub for concurrent programs like web
servers.

Performance Monitoring

You can use Sentry to monitor your application's performance. More information
on the product page https://docs.sentry.io/product/performance/.

The StartSpan function creates new spans.

span := sentry.StartSpan(ctx, "operation")
...
span.Finish()

Integrations

The SDK has support for several Go frameworks, available as subpackages.
Expand Down
125 changes: 108 additions & 17 deletions example/http/main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// This is an example web server to demonstrate how to instrument web servers
// with Sentry.
// This is an example web server to demonstrate how to instrument error and
// performance monitoring with Sentry.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔔 The changes in this file / example should give a taste of what performance instrumentation feels like.

//
// Try it by running:
//
Expand All @@ -20,6 +20,8 @@ import (
"image/png"
"log"
"net/http"
"strings"
"sync"
"time"

"github.com/getsentry/sentry-go"
Expand Down Expand Up @@ -51,6 +53,31 @@ func run() error {
log.Printf("BeforeSend event [%s]", event.EventID)
return event
},
// Specify either TracesSampleRate or set a TracesSampler to
// enable tracing.
// TracesSampleRate: 0.5,
TracesSampler: sentry.TracesSamplerFunc(func(ctx sentry.SamplingContext) sentry.Sampled {
// As an example, this custom sampler does not send some
// transactions to Sentry based on their name.
hub := sentry.GetHubFromContext(ctx.Span.Context())
name := hub.Scope().Transaction()
if name == "GET /favicon.ico" {
return sentry.SampledFalse
}
if strings.HasPrefix(name, "HEAD") {
return sentry.SampledFalse
}
// As an example, sample some transactions with a
// uniform rate.
if strings.HasPrefix(name, "POST") {
return sentry.UniformTracesSampler(0.2).Sample(ctx)
}
// Sample all other transactions for testing. On
// production, use TracesSampleRate with a rate adequate
// for your traffic, or use the SamplingContext to
// customize sampling per-transaction.
return sentry.SampledTrue
}),
})
if err != nil {
return err
Expand All @@ -60,26 +87,62 @@ func run() error {
defer sentry.Flush(2 * time.Second)

// Main HTTP handler, renders an HTML page with a random image.
//
// A new transaction is automatically sent to Sentry when the handler is
// invoked.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Use GetHubFromContext to get a hub associated with the
// current request. Hubs provide data isolation, such that tags,
// breadcrumbs and other attributes are never mixed up across
// requests.
ctx := r.Context()
hub := sentry.GetHubFromContext(ctx)

if r.URL.Path != "/" {
// Use GetHubFromContext to get a hub associated with the current
// request. Hubs provide data isolation, such that tags, breadcrumbs
// and other attributes are never mixed up across requests.
hub := sentry.GetHubFromContext(r.Context())
hub.Scope().SetTag("url", r.URL.Path)
hub.CaptureMessage("Page Not Found")
http.NotFound(w, r)
return
}

err := t.Execute(w, time.Now().UnixNano())
if err != nil {
log.Printf("[%s] %s", r.URL.Path, err)
return
}
// Set a custom transaction name: use "Home" instead of the
// default "/" based on r.URL.Path.
hub.Scope().SetTransaction("Home")

// The next block of code shows how to instrument concurrent
// tasks.
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
span := sentry.StartSpan(ctx, "template.execute")
defer span.Finish()
err := t.Execute(w, time.Now().UnixNano())
if err != nil {
log.Printf("[%s] %s", r.URL.Path, err)
return
}
}()
go func() {
defer wg.Done()
span := sentry.StartSpan(ctx, "sleep")
defer span.Finish()
// For demonstration only, ensure homepage loading takes
// at least 40ms.
time.Sleep(40 * time.Millisecond)
}()
wg.Wait()
})

// HTTP handler for the random image.
//
// A new transaction is automatically sent to Sentry when the handler is
// invoked. We use sentry.StartSpan and span.Finish to create additional
// child spans measuring specific parts of the image computation.
//
// In general, wrap potentially slow parts of your handlers (external
// network calls, CPU-intensive tasks, etc) to help identify where time
// is spent.
http.HandleFunc("/random.png", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var cancel context.CancelFunc
Expand All @@ -90,9 +153,15 @@ func run() error {
}

q := r.URL.Query().Get("q")
img := NewImage(ctx, 128, 128, []byte(q))

span := sentry.StartSpan(ctx, "NewImage")
img := NewImage(span.Context(), 128, 128, []byte(q))
span.Finish()

span = sentry.StartSpan(ctx, "png.Encode")
err := png.Encode(w, img)
span.Finish()

if err != nil {
log.Printf("[%s] %s", r.URL.Path, err)
hub := sentry.GetHubFromContext(ctx)
Expand All @@ -110,10 +179,11 @@ func run() error {

log.Printf("Serving http://%s", *addr)

// Wrap the default mux with Sentry to capture panics and report errors.
// Wrap the default mux with Sentry to capture panics, report errors and
// measure performance.
//
// Alternatively, you can also wrap individual handlers if you need to use
// different options for different parts of your app.
// Alternatively, you can also wrap individual handlers if you need to
// use different options for different parts of your app.
handler := sentryhttp.New(sentryhttp.Options{}).Handle(http.DefaultServeMux)
return http.ListenAndServe(*addr, handler)
}
Expand Down Expand Up @@ -144,17 +214,38 @@ img {

// NewImage returns a random image based on seed, with the given width and
// height.
//
// NewImage uses the context to create spans that measure the performance of its
// internal parts.
func NewImage(ctx context.Context, width, height int, seed []byte) image.Image {
span := sentry.StartSpan(ctx, "sha256")
b := sha256.Sum256(seed)
span.Finish()

img := image.NewGray(image.Rect(0, 0, width, height))

span = sentry.StartSpan(ctx, "img")
defer span.Finish()
for i := 0; i < len(img.Pix); i += len(b) {
select {
case <-ctx.Done():
// Context canceled, abort image generation.
// Spot the bug: the returned image cannot be encoded as PNG and
// will cause an error that will be reported to Sentry.

// Set a tag on the current span.
span.SetTag("canceled", "yes")
// Set a tag on the current transaction.
//
// Note that spans are not designed to be mutated from
// concurrent goroutines. If multiple goroutines may try
// to mutate a span/transaction, for example to set
// tags, use a mutex to synchronize changes, or use a
// channel to communicate the desired changes back into
// the goroutine where the span was created.
sentry.TransactionFromContext(ctx).SetTag("img.canceled", "yes")

// Spot the bug: the returned image cannot be encoded as
// PNG and will cause an error that will be reported to
// Sentry.
return img.SubImage(image.Rect(0, 0, 0, 0))
default:
}
Expand Down