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 echo middleware/integration #722

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

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

Expand Down Expand Up @@ -52,6 +53,24 @@ func main() {
hub.CaptureMessage("User provided unwanted query string, but we recovered just fine")
})
}

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 := sentryecho.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 {
return err
}

return ctx.String(http.StatusOK, "Hello, World!")
})

Expand Down
35 changes: 35 additions & 0 deletions echo/example_test.go
ribice marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package sentryecho_test

import (
"context"
"net/http"

"github.com/getsentry/sentry-go"
sentryecho "github.com/getsentry/sentry-go/echo"
"github.com/labstack/echo/v4"
)

func ExampleGetSpanFromContext() {
router := echo.New()
router.Use(sentryecho.New(sentryecho.Options{}))
router.GET("/", func(c echo.Context) error {
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 := sentryecho.GetSpanFromContext(c)
// 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 {
return err
}

return c.NoContent(http.StatusOK)
})
}
60 changes: 56 additions & 4 deletions echo/sentryecho.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sentryecho

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

Expand All @@ -13,6 +14,7 @@ import (
const sdkIdentifier = "sentry.go.echo"

const valuesKey = "sentry"
const transactionKey = "sentry_transaction"

type handler struct {
repanic bool
Expand All @@ -22,7 +24,7 @@ type handler struct {

type Options struct {
// Repanic configures whether Sentry should repanic after recovery, in most cases it should be set to true,
// as echo includes it's own Recover middleware what handles http responses.
// as echo includes its own Recover middleware what handles http responses.
Repanic bool
// WaitForDelivery configures whether you want to block the request before moving forward with the response.
// Because Echo's Recover handler doesn't restart the application,
Expand Down Expand Up @@ -57,10 +59,51 @@ func (h *handler) handle(next echo.HandlerFunc) echo.HandlerFunc {
client.SetSDKIdentifier(sdkIdentifier)
}

hub.Scope().SetRequest(ctx.Request())
r := ctx.Request()

transactionName := r.URL.Path
transactionSource := sentry.SourceURL

if path := ctx.Path(); path != "" {
transactionName = path
transactionSource = sentry.SourceRoute
}

options := []sentry.SpanOption{
sentry.WithOpName("http.server"),
sentry.ContinueFromRequest(r),
sentry.WithTransactionSource(transactionSource),
}

transaction := sentry.StartTransaction(
sentry.SetHubOnContext(r.Context(), hub),
fmt.Sprintf("%s %s", r.Method, transactionName),
options...,
)

defer func() {
if err := ctx.Get("error"); err != nil {
if httpError, ok := err.(*echo.HTTPError); ok {
transaction.Status = sentry.HTTPtoSpanStatus(httpError.Code)
}
} else {
transaction.Status = sentry.HTTPtoSpanStatus(ctx.Response().Status)
}
transaction.Finish()
}()

hub.Scope().SetRequest(r)
ctx.Set(valuesKey, hub)
defer h.recoverWithSentry(hub, ctx.Request())
return next(ctx)
ctx.Set(transactionKey, transaction)
defer h.recoverWithSentry(hub, r)

err := next(ctx)
if err != nil {
// Store the error so it can be used in the deferred function
ctx.Set("error", err)
}

return err
}
}

Expand All @@ -86,3 +129,12 @@ func GetHubFromContext(ctx echo.Context) *sentry.Hub {
}
return nil
}

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