Skip to content

Commit 5942155

Browse files
aldy505ribicevaind
authored
feat(performance): start transaction for echo middleware/integration (#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ć <[email protected]> Co-authored-by: Ivan Dlugos <[email protected]>
1 parent 3c523e2 commit 5942155

File tree

5 files changed

+327
-6
lines changed

5 files changed

+327
-6
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
### Features
66

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

910
## 0.27.0
1011

_examples/echo/main.go

+19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"fmt"
56
"net/http"
67

@@ -52,6 +53,24 @@ func main() {
5253
hub.CaptureMessage("User provided unwanted query string, but we recovered just fine")
5354
})
5455
}
56+
57+
expensiveThing := func(ctx context.Context) error {
58+
span := sentry.StartTransaction(ctx, "expensive_thing")
59+
defer span.Finish()
60+
// do resource intensive thing
61+
return nil
62+
}
63+
64+
// Acquire transaction on current hub that's created by the SDK.
65+
// Be careful, it might be a nil value if you didn't set up sentryecho middleware.
66+
sentrySpan := sentryecho.GetSpanFromContext(ctx)
67+
// Pass in the `.Context()` method from `*sentry.Span` struct.
68+
// The `context.Context` instance inherits the context from `echo.Context`.
69+
err := expensiveThing(sentrySpan.Context())
70+
if err != nil {
71+
return err
72+
}
73+
5574
return ctx.String(http.StatusOK, "Hello, World!")
5675
})
5776

echo/example_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package sentryecho_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/getsentry/sentry-go"
8+
sentryecho "github.com/getsentry/sentry-go/echo"
9+
"github.com/labstack/echo/v4"
10+
)
11+
12+
func ExampleGetSpanFromContext() {
13+
router := echo.New()
14+
router.Use(sentryecho.New(sentryecho.Options{}))
15+
router.GET("/", func(c echo.Context) error {
16+
expensiveThing := func(ctx context.Context) error {
17+
span := sentry.StartTransaction(ctx, "expensive_thing")
18+
defer span.Finish()
19+
// do resource intensive thing
20+
return nil
21+
}
22+
23+
// Acquire transaction on current hub that's created by the SDK.
24+
// Be careful, it might be a nil value if you didn't set up sentryecho middleware.
25+
sentrySpan := sentryecho.GetSpanFromContext(c)
26+
// Pass in the `.Context()` method from `*sentry.Span` struct.
27+
// The `context.Context` instance inherits the context from `echo.Context`.
28+
err := expensiveThing(sentrySpan.Context())
29+
if err != nil {
30+
return err
31+
}
32+
33+
return c.NoContent(http.StatusOK)
34+
})
35+
}

echo/sentryecho.go

+56-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sentryecho
22

33
import (
44
"context"
5+
"fmt"
56
"net/http"
67
"time"
78

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

1516
const valuesKey = "sentry"
17+
const transactionKey = "sentry_transaction"
1618

1719
type handler struct {
1820
repanic bool
@@ -22,7 +24,7 @@ type handler struct {
2224

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

60-
hub.Scope().SetRequest(ctx.Request())
62+
r := ctx.Request()
63+
64+
transactionName := r.URL.Path
65+
transactionSource := sentry.SourceURL
66+
67+
if path := ctx.Path(); path != "" {
68+
transactionName = path
69+
transactionSource = sentry.SourceRoute
70+
}
71+
72+
options := []sentry.SpanOption{
73+
sentry.WithOpName("http.server"),
74+
sentry.ContinueFromRequest(r),
75+
sentry.WithTransactionSource(transactionSource),
76+
}
77+
78+
transaction := sentry.StartTransaction(
79+
sentry.SetHubOnContext(r.Context(), hub),
80+
fmt.Sprintf("%s %s", r.Method, transactionName),
81+
options...,
82+
)
83+
84+
defer func() {
85+
if err := ctx.Get("error"); err != nil {
86+
if httpError, ok := err.(*echo.HTTPError); ok {
87+
transaction.Status = sentry.HTTPtoSpanStatus(httpError.Code)
88+
}
89+
} else {
90+
transaction.Status = sentry.HTTPtoSpanStatus(ctx.Response().Status)
91+
}
92+
transaction.Finish()
93+
}()
94+
95+
hub.Scope().SetRequest(r)
6196
ctx.Set(valuesKey, hub)
62-
defer h.recoverWithSentry(hub, ctx.Request())
63-
return next(ctx)
97+
ctx.Set(transactionKey, transaction)
98+
defer h.recoverWithSentry(hub, r)
99+
100+
err := next(ctx)
101+
if err != nil {
102+
// Store the error so it can be used in the deferred function
103+
ctx.Set("error", err)
104+
}
105+
106+
return err
64107
}
65108
}
66109

@@ -86,3 +129,12 @@ func GetHubFromContext(ctx echo.Context) *sentry.Hub {
86129
}
87130
return nil
88131
}
132+
133+
// GetSpanFromContext retrieves attached *sentry.Span instance from echo.Context.
134+
// If there is no transaction on echo.Context, it will return nil.
135+
func GetSpanFromContext(ctx echo.Context) *sentry.Span {
136+
if span, ok := ctx.Get(transactionKey).(*sentry.Span); ok {
137+
return span
138+
}
139+
return nil
140+
}

0 commit comments

Comments
 (0)