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: http client integration #876

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Changelog

- Add net/http client integration ([#876](https://github.com/getsentry/sentry-go/pull/876))

## 0.29.0

The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.29.0.
Expand Down
75 changes: 75 additions & 0 deletions _examples/httpclient/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"context"
"fmt"
"io"
"net/http"

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

func main() {
_ = sentry.Init(sentry.ClientOptions{
Dsn: "",
EnableTracing: true,
TracesSampleRate: 1.0,
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
fmt.Println(event)
return event
},
BeforeSendTransaction: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
fmt.Println(event)
return event
},
Debug: true,
})

// With custom HTTP client
ctx := sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone())
httpClient := &http.Client{
Transport: sentryhttpclient.NewSentryRoundTripper(nil),
}

err := getExamplePage(ctx, httpClient)
if err != nil {
panic(err)
}

// With Sentry's HTTP client
err = getExamplePage(ctx, sentryhttpclient.SentryHTTPClient)
if err != nil {
panic(err)
}
}

func getExamplePage(ctx context.Context, httpClient *http.Client) error {
span := sentry.StartSpan(ctx, "getExamplePage")
ctx = span.Context()
defer span.Finish()

request, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
if err != nil {
return err
}

response, err := httpClient.Do(request)
if err != nil {
return err
}
defer func() {
if response.Body != nil {
_ = response.Body.Close()
}
}()

body, err := io.ReadAll(response.Body)
if err != nil {
return err
}

fmt.Println(string(body))

return nil
}
107 changes: 107 additions & 0 deletions httpclient/sentryhttpclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Package sentryhttpclient provides Sentry integration for Requests modules to enable distributed tracing between services.
// It is compatible with `net/http.RoundTripper`.
//
// import sentryhttpclient "github.com/getsentry/sentry-go/httpclient"
//
// roundTrippper := sentryhttpclient.NewSentryRoundTripper(nil, nil)
// client := &http.Client{
// Transport: roundTripper,
// }
//
// request, err := client.Do(request)
package sentryhttpclient

import (
"fmt"
"net/http"

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

// SentryRoundTripTracerOption provides a specific type in which defines the option for SentryRoundTripper.
type SentryRoundTripTracerOption func(*SentryRoundTripper)

// WithTags allows the RoundTripper to includes additional tags.
func WithTags(tags map[string]string) SentryRoundTripTracerOption {
return func(t *SentryRoundTripper) {
for k, v := range tags {
t.tags[k] = v
}
}
}

// WithTag allows the RoundTripper to includes additional tag.
func WithTag(key, value string) SentryRoundTripTracerOption {
return func(t *SentryRoundTripper) {
t.tags[key] = value
}
}

// NewSentryRoundTripper provides a wrapper to existing http.RoundTripper to have required span data and trace headers for outgoing HTTP requests.
//
// - If `nil` is passed to `originalRoundTripper`, it will use http.DefaultTransport instead.
func NewSentryRoundTripper(originalRoundTripper http.RoundTripper, opts ...SentryRoundTripTracerOption) http.RoundTripper {
if originalRoundTripper == nil {
originalRoundTripper = http.DefaultTransport
}

t := &SentryRoundTripper{
originalRoundTripper: originalRoundTripper,
tags: make(map[string]string),
}

for _, opt := range opts {
if opt != nil {
opt(t)
}
}

return t
}

// SentryRoundTripper provides a http.RoundTripper implementation for Sentry Requests module.
type SentryRoundTripper struct {
originalRoundTripper http.RoundTripper

tags map[string]string
}

func (s *SentryRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
// Only create the `http.client` span only if there is a parent span.
parentSpan := sentry.GetSpanFromContext(request.Context())
if parentSpan == nil {
return s.originalRoundTripper.RoundTrip(request)
}

cleanRequestURL := request.URL.Redacted()

span := parentSpan.StartChild("http.client", sentry.WithTransactionName(fmt.Sprintf("%s %s", request.Method, cleanRequestURL)))
span.Tags = s.tags
defer span.Finish()

span.SetData("http.query", request.URL.Query().Encode())
span.SetData("http.fragment", request.URL.Fragment)
span.SetData("http.request.method", request.Method)
span.SetData("server.address", request.URL.Hostname())
span.SetData("server.port", request.URL.Port())

// Always add `Baggage` and `Sentry-Trace` headers.
request.Header.Add("Baggage", span.ToBaggage())
request.Header.Add("Sentry-Trace", span.ToSentryTrace())

response, err := s.originalRoundTripper.RoundTrip(request)

if response != nil {
span.Status = sentry.HTTPtoSpanStatus(response.StatusCode)
span.SetData("http.response.status_code", response.StatusCode)
span.SetData("http.response_content_length", response.ContentLength)
}

return response, err
}

// SentryHTTPClient provides a default HTTP client with SentryRoundTripper included.
// This can be used directly to perform HTTP request.
var SentryHTTPClient = &http.Client{
Transport: NewSentryRoundTripper(http.DefaultTransport),
}
Loading
Loading