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(performance): start transaction for fasthttp integration #723

Merged
merged 10 commits into from
Mar 26, 2024
19 changes: 19 additions & 0 deletions _examples/fasthttp/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"fmt"

"github.com/getsentry/sentry-go"
Expand All @@ -13,6 +14,24 @@ func enhanceSentryEvent(handler fasthttp.RequestHandler) fasthttp.RequestHandler
if hub := sentryfasthttp.GetHubFromContext(ctx); hub != nil {
hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt")
}

expensiveThing := func(ctx context.Context) error {
span := sentry.StartTransaction(ctx, "expensive_thing")
defer span.Finish()
// do resource intensive thing
return nil
}

// Acquire transaction on current hub that's created by the SDK.
// Be careful, it might be a nil value if you didn't set up sentryecho middleware.
sentrySpan := sentryfasthttp.GetSpanFromContext(ctx)
// Pass in the `.Context()` method from `*sentry.Span` struct.
// The `context.Context` instance inherits the context from `echo.Context`.
err := expensiveThing(sentrySpan.Context())
if err != nil {
sentry.CaptureException(err)
}

handler(ctx)
}
}
Expand Down
30 changes: 29 additions & 1 deletion fasthttp/sentryfasthttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type contextKey int

const ContextKey = contextKey(1)
const valuesKey = "sentry"
const transactionKey = "sentry_transaction"

type Handler struct {
repanic bool
Expand Down Expand Up @@ -66,10 +67,28 @@ func (h *Handler) Handle(handler fasthttp.RequestHandler) fasthttp.RequestHandle
client.SetSDKIdentifier(sdkIdentifier)
}

convertedHTTPRequest := convert(ctx)

options := []sentry.SpanOption{
sentry.WithOpName("http.server"),
sentry.ContinueFromRequest(convertedHTTPRequest),
sentry.WithTransactionSource(sentry.SourceRoute),
}
transaction := sentry.StartTransaction(
sentry.SetHubOnContext(ctx, hub),
vaind marked this conversation as resolved.
Show resolved Hide resolved
fmt.Sprintf("%s %s", string(ctx.Method()), string(ctx.Path())),
options...,
)
defer func() {
transaction.Status = sentry.HTTPtoSpanStatus(ctx.Response.StatusCode())
transaction.Finish()
}()

scope := hub.Scope()
scope.SetRequest(convert(ctx))
scope.SetRequest(convertedHTTPRequest)
scope.SetRequestBody(ctx.Request.Body())
ctx.SetUserValue(valuesKey, hub)
ctx.SetUserValue(transactionKey, transaction)
defer h.recoverWithSentry(hub, ctx)
handler(ctx)
}
Expand Down Expand Up @@ -99,6 +118,15 @@ func GetHubFromContext(ctx *fasthttp.RequestCtx) *sentry.Hub {
return nil
}

// GetSpanFromContext retrieves attached *sentry.Span instance from *fasthttp.RequestCtx.
// If there is no transaction on *fasthttp.RequestCtx, it will return nil.
func GetSpanFromContext(ctx *fasthttp.RequestCtx) *sentry.Span {
if span, ok := ctx.UserValue(transactionKey).(*sentry.Span); ok {
return span
}
return nil
}

func convert(ctx *fasthttp.RequestCtx) *http.Request {
defer func() {
if err := recover(); err != nil {
Expand Down
223 changes: 216 additions & 7 deletions fasthttp/sentryfasthttp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ func TestIntegration(t *testing.T) {
Body string
Handler fasthttp.RequestHandler

WantEvent *sentry.Event
WantEvent *sentry.Event
WantTransaction *sentry.Event
}{
{
Path: "/panic",
Expand All @@ -46,6 +47,20 @@ func TestIntegration(t *testing.T) {
},
},
},
WantTransaction: &sentry.Event{
Level: sentry.LevelInfo,
Type: "transaction",
Transaction: "GET /panic",
Request: &sentry.Request{
URL: "http://example.com/panic",
Method: "GET",
Headers: map[string]string{
"Host": "example.com",
"User-Agent": "fasthttp",
},
},
TransactionInfo: &sentry.TransactionInfo{Source: "route"},
},
},
{
Path: "/post",
Expand All @@ -69,6 +84,21 @@ func TestIntegration(t *testing.T) {
},
},
},
WantTransaction: &sentry.Event{
Level: sentry.LevelInfo,
Type: "transaction",
Transaction: "POST /post",
Request: &sentry.Request{
URL: "http://example.com/post",
Method: "POST",
Data: "payload",
Headers: map[string]string{
"Host": "example.com",
"User-Agent": "fasthttp",
},
},
TransactionInfo: &sentry.TransactionInfo{Source: "route"},
},
},
{
Path: "/get",
Expand All @@ -89,6 +119,20 @@ func TestIntegration(t *testing.T) {
},
},
},
WantTransaction: &sentry.Event{
Level: sentry.LevelInfo,
Type: "transaction",
Transaction: "GET /get",
Request: &sentry.Request{
URL: "http://example.com/get",
Method: "GET",
Headers: map[string]string{
"Host": "example.com",
"User-Agent": "fasthttp",
},
},
TransactionInfo: &sentry.TransactionInfo{Source: "route"},
},
},
{
Path: "/post/large",
Expand All @@ -113,6 +157,21 @@ func TestIntegration(t *testing.T) {
},
},
},
WantTransaction: &sentry.Event{
Level: sentry.LevelInfo,
Type: "transaction",
Transaction: "POST /post/large",
Request: &sentry.Request{
URL: "http://example.com/post/large",
Method: "POST",
Data: "",
Headers: map[string]string{
"Host": "example.com",
"User-Agent": "fasthttp",
},
},
TransactionInfo: &sentry.TransactionInfo{Source: "route"},
},
},
{
Path: "/post/body-ignored",
Expand All @@ -138,15 +197,37 @@ func TestIntegration(t *testing.T) {
},
},
},
WantTransaction: &sentry.Event{
Level: sentry.LevelInfo,
Type: "transaction",
Transaction: "POST /post/body-ignored",
Request: &sentry.Request{
URL: "http://example.com/post/body-ignored",
Method: "POST",
Data: "client sends, fasthttp always reads, SDK reports",
Headers: map[string]string{
"Host": "example.com",
"User-Agent": "fasthttp",
},
},
TransactionInfo: &sentry.TransactionInfo{Source: "route"},
},
},
}

eventsCh := make(chan *sentry.Event, len(tests))
transactionsCh := make(chan *sentry.Event, len(tests))
err := sentry.Init(sentry.ClientOptions{
EnableTracing: true,
TracesSampleRate: 1.0,
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
eventsCh <- event
return event
},
BeforeSendTransaction: func(tx *sentry.Event, hint *sentry.EventHint) *sentry.Event {
transactionsCh <- tx
return tx
},
})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -179,9 +260,11 @@ func TestIntegration(t *testing.T) {
WriteTimeout: time.Second,
}

var want []*sentry.Event
var wantEvents []*sentry.Event
var wantTransactions []*sentry.Event
for _, tt := range tests {
want = append(want, tt.WantEvent)
wantEvents = append(wantEvents, tt.WantEvent)
wantTransactions = append(wantTransactions, tt.WantTransaction)
req, res := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
req.SetHost("example.com")
req.URI().SetPath(tt.Path)
Expand All @@ -198,11 +281,13 @@ func TestIntegration(t *testing.T) {
if ok := sentry.Flush(testutils.FlushTimeout()); !ok {
t.Fatal("sentry.Flush timed out")
}

close(eventsCh)
var got []*sentry.Event
var gotEvents []*sentry.Event
for e := range eventsCh {
got = append(got, e)
gotEvents = append(gotEvents, e)
}

opts := cmp.Options{
cmpopts.IgnoreFields(
sentry.Event{},
Expand All @@ -221,10 +306,134 @@ func TestIntegration(t *testing.T) {
return k == "Content-Length" || k == "Content-Type"
}),
}
if diff := cmp.Diff(want, got, opts); diff != "" {
t.Fatalf("Events mismatch (-want +got):\n%s", diff)
if diff := cmp.Diff(wantEvents, gotEvents, opts); diff != "" {
t.Fatalf("Events mismatch (-want +gotEvents):\n%s", diff)
}

close(transactionsCh)
var gotTransactions []*sentry.Event
for e := range transactionsCh {
gotTransactions = append(gotTransactions, e)
}
optstrans := cmp.Options{
cmpopts.IgnoreFields(
sentry.Event{},
"Contexts", "EventID", "Platform", "Modules",
"Release", "Sdk", "ServerName", "Timestamp",
"sdkMetaData", "StartTime", "Spans",
),
cmpopts.IgnoreFields(
sentry.Request{},
"Env",
),
cmpopts.IgnoreMapEntries(func(k string, v string) bool {
// fasthttp changed Content-Length behavior in
// https://github.com/valyala/fasthttp/commit/097fa05a697fc638624a14ab294f1336da9c29b0.
// fasthttp changed Content-Type behavior in
// https://github.com/valyala/fasthttp/commit/ffa0cabed8199819e372ebd2c739998914150ff2.
// Since the specific values of those headers are not
// important from the perspective of sentry-go, we
// ignore them.
return k == "Content-Length" || k == "Content-Type"
}),
}
if diff := cmp.Diff(wantTransactions, gotTransactions, optstrans); diff != "" {
t.Fatalf("Transactions mismatch (-want +gotEvents):\n%s", diff)
}

ln.Close()
<-done
}

func TestGetTransactionFromContext(t *testing.T) {
err := sentry.Init(sentry.ClientOptions{
EnableTracing: true,
TracesSampleRate: 1.0,
})
if err != nil {
t.Fatal(err)
}

t.Run("With Transaction", func(t *testing.T) {
sentryHandler := sentryfasthttp.New(sentryfasthttp.Options{})
ln := fasthttputil.NewInmemoryListener()
handler := func(ctx *fasthttp.RequestCtx) {
span := sentryfasthttp.GetSpanFromContext(ctx)
if span == nil {
t.Error("expecting span to be not nil")
}

ctx.SetStatusCode(200)
}
done := make(chan struct{})
go func() {
if err := fasthttp.Serve(ln, sentryHandler.Handle(handler)); err != nil {
t.Errorf("error in Serve: %s", err)
}
close(done)
}()

c := &fasthttp.Client{
Dial: func(addr string) (net.Conn, error) {
return ln.Dial()
},
ReadTimeout: time.Second,
WriteTimeout: time.Second,
}

req, res := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
req.SetHost("example.com")
req.URI().SetPath("/")
req.Header.SetMethod("GET")
if err := c.Do(req, res); err != nil {
t.Fatalf("Request failed: %s", err)
}
if res.StatusCode() != http.StatusOK {
t.Errorf("Status code = %d", res.StatusCode())
}

ln.Close()
<-done
})

t.Run("Without Transaction", func(t *testing.T) {
ln := fasthttputil.NewInmemoryListener()
handler := func(ctx *fasthttp.RequestCtx) {
span := sentryfasthttp.GetSpanFromContext(ctx)
if span != nil {
t.Error("expecting span to be nil")
}

ctx.SetStatusCode(200)
}
done := make(chan struct{})
go func() {
if err := fasthttp.Serve(ln, handler); err != nil {
t.Errorf("error in Serve: %s", err)
}
close(done)
}()

c := &fasthttp.Client{
Dial: func(addr string) (net.Conn, error) {
return ln.Dial()
},
ReadTimeout: time.Second,
WriteTimeout: time.Second,
}

req, res := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
req.SetHost("example.com")
req.URI().SetPath("/")
req.Header.SetMethod("GET")
if err := c.Do(req, res); err != nil {
t.Fatalf("Request failed: %s", err)
}
if res.StatusCode() != http.StatusOK {
t.Errorf("Status code = %d", res.StatusCode())
}

ln.Close()
<-done
})
}
Loading