Skip to content

Commit aed9115

Browse files
authored
feat: Initial tracing implementation (#285)
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.
1 parent 6c276c3 commit aed9115

17 files changed

+1177
-114
lines changed

README.md

Lines changed: 6 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -65,88 +65,13 @@ More on this in the [Configuration section of the official Sentry Go SDK documen
6565

6666
## Usage
6767

68-
The SDK must be initialized with a call to `sentry.Init`. The default transport
69-
is asynchronous and thus most programs should call `sentry.Flush` to wait until
70-
buffered events are sent to Sentry right before the program terminates.
71-
72-
Typically, `sentry.Init` is called in the beginning of `func main` and
73-
`sentry.Flush` is [deferred](https://golang.org/ref/spec#Defer_statements) right
74-
after.
75-
76-
> Note that if the program terminates with a call to
77-
> [`os.Exit`](https://golang.org/pkg/os/#Exit), either directly or indirectly
78-
> via another function like `log.Fatal`, deferred functions are not run.
79-
>
80-
> In that case, and if it is important for you to report outstanding events
81-
> before terminating the program, arrange for `sentry.Flush` to be called before
82-
> the program terminates.
83-
84-
Example:
85-
86-
```go
87-
// This is an example program that makes an HTTP request and prints response
88-
// headers. Whenever a request fails, the error is reported to Sentry.
89-
//
90-
// Try it by running:
91-
//
92-
// go run main.go
93-
// go run main.go https://sentry.io
94-
// go run main.go bad-url
95-
//
96-
// To actually report events to Sentry, set the DSN either by editing the
97-
// appropriate line below or setting the environment variable SENTRY_DSN to
98-
// match the DSN of your Sentry project.
99-
package main
100-
101-
import (
102-
"fmt"
103-
"log"
104-
"net/http"
105-
"os"
106-
"time"
107-
108-
"github.com/getsentry/sentry-go"
109-
)
110-
111-
func main() {
112-
if len(os.Args) < 2 {
113-
log.Fatalf("usage: %s URL", os.Args[0])
114-
}
115-
116-
err := sentry.Init(sentry.ClientOptions{
117-
// Either set your DSN here or set the SENTRY_DSN environment variable.
118-
Dsn: "",
119-
// Enable printing of SDK debug messages.
120-
// Useful when getting started or trying to figure something out.
121-
Debug: true,
122-
})
123-
if err != nil {
124-
log.Fatalf("sentry.Init: %s", err)
125-
}
126-
// Flush buffered events before the program terminates.
127-
// Set the timeout to the maximum duration the program can afford to wait.
128-
defer sentry.Flush(2 * time.Second)
129-
130-
resp, err := http.Get(os.Args[1])
131-
if err != nil {
132-
sentry.CaptureException(err)
133-
log.Printf("reported to Sentry: %s", err)
134-
return
135-
}
136-
defer resp.Body.Close()
137-
138-
for header, values := range resp.Header {
139-
for _, value := range values {
140-
fmt.Printf("%s=%s\n", header, value)
141-
}
142-
}
143-
}
144-
```
68+
The SDK supports reporting errors and tracking application performance.
69+
70+
To get started, have a look at one of our [examples](example/):
71+
- [Basic error instrumentation](example/basic/main.go)
72+
- [Error and tracing for HTTP servers](example/http/main.go)
14573

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

15176
For more detailed information about how to get the most out of `sentry-go`,
15277
checkout the official documentation:

client.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sentry
33
import (
44
"context"
55
"crypto/x509"
6+
"errors"
67
"fmt"
78
"io"
89
"io/ioutil"
@@ -119,6 +120,10 @@ type ClientOptions struct {
119120
// 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the
120121
// empty string.
121122
SampleRate float64
123+
// The sample rate for sampling traces in the range [0.0, 1.0].
124+
TracesSampleRate float64
125+
// Used to customize the sampling of traces, overrides TracesSampleRate.
126+
TracesSampler TracesSampler
122127
// List of regexp strings that will be used to match against event's message
123128
// and if applicable, caught errors type and value.
124129
// If the match is found, then a whole event will be dropped.
@@ -183,6 +188,10 @@ type Client struct {
183188
// single goroutine) or hub methods (for concurrent programs, for example web
184189
// servers).
185190
func NewClient(options ClientOptions) (*Client, error) {
191+
if options.TracesSampleRate != 0.0 && options.TracesSampler != nil {
192+
return nil, errors.New("TracesSampleRate and TracesSampler are mutually exclusive")
193+
}
194+
186195
if options.Debug {
187196
debugWriter := options.DebugWriter
188197
if debugWriter == nil {
@@ -231,17 +240,26 @@ func NewClient(options ClientOptions) (*Client, error) {
231240
}
232241

233242
func (client *Client) setupTransport() {
234-
transport := client.options.Transport
243+
opts := client.options
244+
transport := opts.Transport
235245

236246
if transport == nil {
237-
if client.options.Dsn == "" {
247+
if opts.Dsn == "" {
238248
transport = new(noopTransport)
239249
} else {
240-
transport = NewHTTPTransport()
250+
httpTransport := NewHTTPTransport()
251+
// When tracing is enabled, use larger buffer to
252+
// accommodate more concurrent events.
253+
// TODO(tracing): consider using separate buffers per
254+
// event type.
255+
if opts.TracesSampleRate != 0 || opts.TracesSampler != nil {
256+
httpTransport.BufferSize = 1000
257+
}
258+
transport = httpTransport
241259
}
242260
}
243261

244-
transport.Configure(client.options)
262+
transport.Configure(opts)
245263
client.Transport = transport
246264
}
247265

doc.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/*
22
Package sentry is the official Sentry SDK for Go.
33
4+
Use it to report errors and track application performance through distributed
5+
tracing.
6+
47
For more information about Sentry and SDK features please have a look at the
58
documentation site https://docs.sentry.io/platforms/go/.
69
@@ -17,6 +20,28 @@ Sentry project. This step is accomplished through a call to sentry.Init.
1720
A more detailed yet simple example is available at
1821
https://github.com/getsentry/sentry-go/blob/master/example/basic/main.go.
1922
23+
Error Reporting
24+
25+
The Capture* functions report messages and errors to Sentry.
26+
27+
sentry.CaptureMessage(...)
28+
sentry.CaptureException(...)
29+
sentry.CaptureEvent(...)
30+
31+
Use similarly named functions in the Hub for concurrent programs like web
32+
servers.
33+
34+
Performance Monitoring
35+
36+
You can use Sentry to monitor your application's performance. More information
37+
on the product page https://docs.sentry.io/product/performance/.
38+
39+
The StartSpan function creates new spans.
40+
41+
span := sentry.StartSpan(ctx, "operation")
42+
...
43+
span.Finish()
44+
2045
Integrations
2146
2247
The SDK has support for several Go frameworks, available as subpackages.

example/http/main.go

Lines changed: 108 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// This is an example web server to demonstrate how to instrument web servers
2-
// with Sentry.
1+
// This is an example web server to demonstrate how to instrument error and
2+
// performance monitoring with Sentry.
33
//
44
// Try it by running:
55
//
@@ -20,6 +20,8 @@ import (
2020
"image/png"
2121
"log"
2222
"net/http"
23+
"strings"
24+
"sync"
2325
"time"
2426

2527
"github.com/getsentry/sentry-go"
@@ -51,6 +53,31 @@ func run() error {
5153
log.Printf("BeforeSend event [%s]", event.EventID)
5254
return event
5355
},
56+
// Specify either TracesSampleRate or set a TracesSampler to
57+
// enable tracing.
58+
// TracesSampleRate: 0.5,
59+
TracesSampler: sentry.TracesSamplerFunc(func(ctx sentry.SamplingContext) sentry.Sampled {
60+
// As an example, this custom sampler does not send some
61+
// transactions to Sentry based on their name.
62+
hub := sentry.GetHubFromContext(ctx.Span.Context())
63+
name := hub.Scope().Transaction()
64+
if name == "GET /favicon.ico" {
65+
return sentry.SampledFalse
66+
}
67+
if strings.HasPrefix(name, "HEAD") {
68+
return sentry.SampledFalse
69+
}
70+
// As an example, sample some transactions with a
71+
// uniform rate.
72+
if strings.HasPrefix(name, "POST") {
73+
return sentry.UniformTracesSampler(0.2).Sample(ctx)
74+
}
75+
// Sample all other transactions for testing. On
76+
// production, use TracesSampleRate with a rate adequate
77+
// for your traffic, or use the SamplingContext to
78+
// customize sampling per-transaction.
79+
return sentry.SampledTrue
80+
}),
5481
})
5582
if err != nil {
5683
return err
@@ -60,26 +87,62 @@ func run() error {
6087
defer sentry.Flush(2 * time.Second)
6188

6289
// Main HTTP handler, renders an HTML page with a random image.
90+
//
91+
// A new transaction is automatically sent to Sentry when the handler is
92+
// invoked.
6393
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
94+
// Use GetHubFromContext to get a hub associated with the
95+
// current request. Hubs provide data isolation, such that tags,
96+
// breadcrumbs and other attributes are never mixed up across
97+
// requests.
98+
ctx := r.Context()
99+
hub := sentry.GetHubFromContext(ctx)
100+
64101
if r.URL.Path != "/" {
65-
// Use GetHubFromContext to get a hub associated with the current
66-
// request. Hubs provide data isolation, such that tags, breadcrumbs
67-
// and other attributes are never mixed up across requests.
68-
hub := sentry.GetHubFromContext(r.Context())
69102
hub.Scope().SetTag("url", r.URL.Path)
70103
hub.CaptureMessage("Page Not Found")
71104
http.NotFound(w, r)
72105
return
73106
}
74107

75-
err := t.Execute(w, time.Now().UnixNano())
76-
if err != nil {
77-
log.Printf("[%s] %s", r.URL.Path, err)
78-
return
79-
}
108+
// Set a custom transaction name: use "Home" instead of the
109+
// default "/" based on r.URL.Path.
110+
hub.Scope().SetTransaction("Home")
111+
112+
// The next block of code shows how to instrument concurrent
113+
// tasks.
114+
var wg sync.WaitGroup
115+
wg.Add(2)
116+
go func() {
117+
defer wg.Done()
118+
span := sentry.StartSpan(ctx, "template.execute")
119+
defer span.Finish()
120+
err := t.Execute(w, time.Now().UnixNano())
121+
if err != nil {
122+
log.Printf("[%s] %s", r.URL.Path, err)
123+
return
124+
}
125+
}()
126+
go func() {
127+
defer wg.Done()
128+
span := sentry.StartSpan(ctx, "sleep")
129+
defer span.Finish()
130+
// For demonstration only, ensure homepage loading takes
131+
// at least 40ms.
132+
time.Sleep(40 * time.Millisecond)
133+
}()
134+
wg.Wait()
80135
})
81136

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

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

157+
span := sentry.StartSpan(ctx, "NewImage")
158+
img := NewImage(span.Context(), 128, 128, []byte(q))
159+
span.Finish()
160+
161+
span = sentry.StartSpan(ctx, "png.Encode")
95162
err := png.Encode(w, img)
163+
span.Finish()
164+
96165
if err != nil {
97166
log.Printf("[%s] %s", r.URL.Path, err)
98167
hub := sentry.GetHubFromContext(ctx)
@@ -110,10 +179,11 @@ func run() error {
110179

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

113-
// Wrap the default mux with Sentry to capture panics and report errors.
182+
// Wrap the default mux with Sentry to capture panics, report errors and
183+
// measure performance.
114184
//
115-
// Alternatively, you can also wrap individual handlers if you need to use
116-
// different options for different parts of your app.
185+
// Alternatively, you can also wrap individual handlers if you need to
186+
// use different options for different parts of your app.
117187
handler := sentryhttp.New(sentryhttp.Options{}).Handle(http.DefaultServeMux)
118188
return http.ListenAndServe(*addr, handler)
119189
}
@@ -144,17 +214,38 @@ img {
144214

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

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

227+
span = sentry.StartSpan(ctx, "img")
228+
defer span.Finish()
152229
for i := 0; i < len(img.Pix); i += len(b) {
153230
select {
154231
case <-ctx.Done():
155232
// Context canceled, abort image generation.
156-
// Spot the bug: the returned image cannot be encoded as PNG and
157-
// will cause an error that will be reported to Sentry.
233+
234+
// Set a tag on the current span.
235+
span.SetTag("canceled", "yes")
236+
// Set a tag on the current transaction.
237+
//
238+
// Note that spans are not designed to be mutated from
239+
// concurrent goroutines. If multiple goroutines may try
240+
// to mutate a span/transaction, for example to set
241+
// tags, use a mutex to synchronize changes, or use a
242+
// channel to communicate the desired changes back into
243+
// the goroutine where the span was created.
244+
sentry.TransactionFromContext(ctx).SetTag("img.canceled", "yes")
245+
246+
// Spot the bug: the returned image cannot be encoded as
247+
// PNG and will cause an error that will be reported to
248+
// Sentry.
158249
return img.SubImage(image.Rect(0, 0, 0, 0))
159250
default:
160251
}

0 commit comments

Comments
 (0)