Skip to content

Commit

Permalink
feat: Initial tracing implementation
Browse files Browse the repository at this point in the history
This adds the StartSpan function and related APIs to the SDK.

The initial support focuses on manual instrumentation and HTTP servers
based on net/http.

Tracing is opt-in. Use one of the new options, TracesSampleRate or
TracesSampler, when initializing the SDK to enable sending transactions
and spans to Sentry.

The tracing APIs rely heavily on the standard Context type from Go, and
integrate with the SDKs notion of scopes.

See example/http/main.go for an example of how the new APIs are meant to
be used in practice.

While the basic functionality should be in place, more features are
planned for later.
  • Loading branch information
rhcarvalho committed Dec 2, 2020
1 parent 18aaf7e commit cbab8df
Show file tree
Hide file tree
Showing 17 changed files with 1,028 additions and 106 deletions.
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)
}
}
}
```
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
4 changes: 4 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,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
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.CaptureException(...)
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
97 changes: 84 additions & 13 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.
//
// Try it by running:
//
Expand All @@ -20,6 +20,7 @@ import (
"image/png"
"log"
"net/http"
"sync"
"time"

"github.com/getsentry/sentry-go"
Expand Down Expand Up @@ -51,6 +52,15 @@ 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) bool {
// Sample all transactions for testing. On production, use
// TracesSampleRate with a rate adequate for your traffic, or use
// the ctx in TracesSampler to customize sampling per-transaction..
return true
}),
})
if err != nil {
return err
Expand All @@ -60,26 +70,60 @@ 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.
hub := sentry.GetHubFromContext(r.Context())
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(r.Context(), "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(r.Context(), "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 +134,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,7 +160,8 @@ 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.
Expand Down Expand Up @@ -144,15 +195,35 @@ 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.

// 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))
Expand Down
13 changes: 11 additions & 2 deletions http/sentryhttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,20 @@ func (h *Handler) handle(handler http.Handler) http.HandlerFunc {
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
hub = sentry.CurrentHub().Clone()
ctx = sentry.SetHubOnContext(ctx, hub)
}
span := sentry.StartSpan(ctx, "http.server",
sentry.TransactionName(r.URL.Path),
sentry.ContinueFromRequest(r),
)
defer span.Finish()
r = r.WithContext(span.Context())
hub.Scope().SetRequest(r)
ctx = sentry.SetHubOnContext(ctx, hub)
defer h.recoverWithSentry(hub, r)
handler.ServeHTTP(w, r.WithContext(ctx))
// TODO(tracing): use custom response writer to intercept
// response. Use HTTP status to add tag to transaction; set span
// status.
handler.ServeHTTP(w, r)
}
}

Expand Down
5 changes: 5 additions & 0 deletions http/sentryhttp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func TestIntegration(t *testing.T) {
"User-Agent": "Go-http-client/1.1",
},
},
Transaction: "/panic",
},
},
{
Expand Down Expand Up @@ -71,6 +72,7 @@ func TestIntegration(t *testing.T) {
"User-Agent": "Go-http-client/1.1",
},
},
Transaction: "/post",
},
},
{
Expand All @@ -91,6 +93,7 @@ func TestIntegration(t *testing.T) {
"User-Agent": "Go-http-client/1.1",
},
},
Transaction: "/get",
},
},
{
Expand Down Expand Up @@ -120,6 +123,7 @@ func TestIntegration(t *testing.T) {
"User-Agent": "Go-http-client/1.1",
},
},
Transaction: "/post/large",
},
},
{
Expand All @@ -145,6 +149,7 @@ func TestIntegration(t *testing.T) {
"User-Agent": "Go-http-client/1.1",
},
},
Transaction: "/post/body-ignored",
},
},
}
Expand Down
9 changes: 9 additions & 0 deletions hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,15 @@ func GetHubFromContext(ctx context.Context) *Hub {
return nil
}

// hubFromContext returns either a hub stored in the context or the current hub.
// The return value is guaranteed to be non-nil, unlike GetHubFromContext.
func hubFromContext(ctx context.Context) *Hub {
if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
return hub
}
return currentHub
}

// SetHubOnContext stores given Hub instance on the Context struct and returns a new Context.
func SetHubOnContext(ctx context.Context, hub *Hub) context.Context {
return context.WithValue(ctx, HubContextKey, hub)
Expand Down

0 comments on commit cbab8df

Please sign in to comment.