Skip to content

Commit d7582e8

Browse files
authored
feat: add http client integration (#876)
1 parent 1ce3436 commit d7582e8

File tree

4 files changed

+689
-0
lines changed

4 files changed

+689
-0
lines changed

_examples/httpclient/main.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
9+
"github.com/getsentry/sentry-go"
10+
sentryhttpclient "github.com/getsentry/sentry-go/httpclient"
11+
)
12+
13+
func main() {
14+
_ = sentry.Init(sentry.ClientOptions{
15+
Dsn: "",
16+
EnableTracing: true,
17+
TracesSampleRate: 1.0,
18+
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
19+
fmt.Println(event)
20+
return event
21+
},
22+
BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
23+
fmt.Println(event)
24+
return event
25+
},
26+
Debug: true,
27+
})
28+
29+
// With custom HTTP client
30+
ctx := sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone())
31+
httpClient := &http.Client{
32+
Transport: sentryhttpclient.NewSentryRoundTripper(nil),
33+
}
34+
35+
err := getExamplePage(ctx, httpClient)
36+
if err != nil {
37+
panic(err)
38+
}
39+
}
40+
41+
func getExamplePage(ctx context.Context, httpClient *http.Client) error {
42+
span := sentry.StartSpan(ctx, "getExamplePage")
43+
ctx = span.Context()
44+
defer span.Finish()
45+
46+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
47+
if err != nil {
48+
return err
49+
}
50+
51+
response, err := httpClient.Do(request)
52+
if err != nil {
53+
return err
54+
}
55+
defer func() {
56+
if response.Body != nil {
57+
_ = response.Body.Close()
58+
}
59+
}()
60+
61+
body, err := io.ReadAll(response.Body)
62+
if err != nil {
63+
return err
64+
}
65+
66+
fmt.Println(string(body))
67+
68+
return nil
69+
}

client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ type ClientOptions struct {
143143
TracesSampleRate float64
144144
// Used to customize the sampling of traces, overrides TracesSampleRate.
145145
TracesSampler TracesSampler
146+
// Control with URLs trace propagation should be enabled. Does not support regex patterns.
147+
TracePropagationTargets []string
146148
// List of regexp strings that will be used to match against event's message
147149
// and if applicable, caught errors type and value.
148150
// If the match is found, then a whole event will be dropped.

httpclient/sentryhttpclient.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Package sentryhttpclient provides Sentry integration for Requests modules to enable distributed tracing between services.
2+
// It is compatible with `net/http.RoundTripper`.
3+
//
4+
// import sentryhttpclient "github.com/getsentry/sentry-go/httpclient"
5+
//
6+
// roundTrippper := sentryhttpclient.NewSentryRoundTripper(nil, nil)
7+
// client := &http.Client{
8+
// Transport: roundTripper,
9+
// }
10+
//
11+
// request, err := client.Do(request)
12+
package sentryhttpclient
13+
14+
import (
15+
"fmt"
16+
"net/http"
17+
"strings"
18+
19+
"github.com/getsentry/sentry-go"
20+
)
21+
22+
// SentryRoundTripTracerOption provides a specific type in which defines the option for SentryRoundTripper.
23+
type SentryRoundTripTracerOption func(*SentryRoundTripper)
24+
25+
// WithTracePropagationTargets configures additional trace propagation targets URL for the RoundTripper.
26+
// Does not support regex patterns.
27+
func WithTracePropagationTargets(targets []string) SentryRoundTripTracerOption {
28+
return func(t *SentryRoundTripper) {
29+
if t.tracePropagationTargets == nil {
30+
t.tracePropagationTargets = targets
31+
} else {
32+
t.tracePropagationTargets = append(t.tracePropagationTargets, targets...)
33+
}
34+
}
35+
}
36+
37+
// NewSentryRoundTripper provides a wrapper to existing http.RoundTripper to have required span data and trace headers for outgoing HTTP requests.
38+
//
39+
// - If `nil` is passed to `originalRoundTripper`, it will use http.DefaultTransport instead.
40+
func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...SentryRoundTripTracerOption) http.RoundTripper {
41+
if originalRoundTripper == nil {
42+
originalRoundTripper = http.DefaultTransport
43+
}
44+
45+
// Configure trace propagation targets
46+
var tracePropagationTargets []string
47+
if hub := sentry.CurrentHub(); hub != nil {
48+
client := hub.Client()
49+
if client != nil {
50+
clientOptions := client.Options()
51+
if clientOptions.TracePropagationTargets != nil {
52+
tracePropagationTargets = clientOptions.TracePropagationTargets
53+
}
54+
}
55+
}
56+
57+
t := &SentryRoundTripper{
58+
originalRoundTripper: originalRoundTripper,
59+
tracePropagationTargets: tracePropagationTargets,
60+
}
61+
62+
for _, opt := range opts {
63+
if opt != nil {
64+
opt(t)
65+
}
66+
}
67+
68+
return t
69+
}
70+
71+
// SentryRoundTripper provides a http.RoundTripper implementation for Sentry Requests module.
72+
type SentryRoundTripper struct {
73+
originalRoundTripper http.RoundTripper
74+
75+
tracePropagationTargets []string
76+
}
77+
78+
func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
79+
// Respect trace propagation targets
80+
if len(s.tracePropagationTargets) > 0 {
81+
requestURL := request.URL.String()
82+
foundMatch := false
83+
for _, target := range s.tracePropagationTargets {
84+
if strings.Contains(requestURL, target) {
85+
foundMatch = true
86+
break
87+
}
88+
}
89+
90+
if !foundMatch {
91+
return s.originalRoundTripper.RoundTrip(request)
92+
}
93+
}
94+
95+
// Only create the `http.client` span only if there is a parent span.
96+
parentSpan := sentry.SpanFromContext(request.Context())
97+
if parentSpan == nil {
98+
if hub := sentry.GetHubFromContext(request.Context()); hub != nil {
99+
request.Header.Add("Baggage", hub.GetBaggage())
100+
request.Header.Add("Sentry-Trace", hub.GetTraceparent())
101+
}
102+
103+
return s.originalRoundTripper.RoundTrip(request)
104+
}
105+
106+
cleanRequestURL := request.URL.Redacted()
107+
108+
span := parentSpan.StartChild("http.client", sentry.WithDescription(fmt.Sprintf("%s %s", request.Method, cleanRequestURL)))
109+
defer span.Finish()
110+
111+
span.SetData("http.query", request.URL.Query().Encode())
112+
span.SetData("http.fragment", request.URL.Fragment)
113+
span.SetData("http.request.method", request.Method)
114+
span.SetData("server.address", request.URL.Hostname())
115+
span.SetData("server.port", request.URL.Port())
116+
117+
// Always add `Baggage` and `Sentry-Trace` headers.
118+
request.Header.Add("Baggage", span.ToBaggage())
119+
request.Header.Add("Sentry-Trace", span.ToSentryTrace())
120+
121+
response, err := s.originalRoundTripper.RoundTrip(request)
122+
if err != nil {
123+
span.Status = sentry.SpanStatusInternalError
124+
return response, err
125+
}
126+
127+
if response != nil {
128+
span.Status = sentry.HTTPtoSpanStatus(response.StatusCode)
129+
span.SetData("http.response.status_code", response.StatusCode)
130+
span.SetData("http.response_content_length", response.ContentLength)
131+
}
132+
133+
return response, err
134+
}

0 commit comments

Comments
 (0)