Skip to content

Commit

Permalink
feat(performance): start transaction for echo middleware/integration (#…
Browse files Browse the repository at this point in the history
…722)

* feat(performance): start transaction for echo middleware/integration

* test(performance): GetTransactionFromContext test for sentryecho middleware

* chore(echo): rename GetTransactionFromContext to GetSpanFromContext

* chore(echo): move examples to _examples directory

* handle 404 errors

* add TestGetTransactionFromContext

* Update CHANGELOG.md

---------

Co-authored-by: Emir Ribić <e@ribic.ba>
Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 26, 2024
1 parent 3c523e2 commit 5942155
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 6 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

### Features

- accept `interface{}` for span data values ([#784](https://github.com/getsentry/sentry-go/pull/784))
- Accept `interface{}` for span data values ([#784](https://github.com/getsentry/sentry-go/pull/784))
- Automatic transactions for Echo integration ([#722](https://github.com/getsentry/sentry-go/pull/722))

## 0.27.0

Expand Down
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
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

0 comments on commit 5942155

Please sign in to comment.