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

Add integration with buffalo #196

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions buffalo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<p align="center">
<a href="https://sentry.io" target="_blank" align="center">
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280">
</a>
<br />
</p>

# Official Sentry Buffalo Middleware for Sentry-go SDK

**Godoc:** https://godoc.org/github.com/getsentry/sentry-go/buffalo

**Example:** https://github.com/getsentry/sentry-go/tree/master/example/buffalo

## Installation

```sh
go get github.com/getsentry/sentry-go/buffalo
```

```go
package actions

import (
"github.com/gobuffalo/buffalo"
"github.com/gobuffalo/envy"
forcessl "github.com/gobuffalo/mw-forcessl"
paramlogger "github.com/gobuffalo/mw-paramlogger"
"github.com/unrolled/secure"
sentrybuffalo "github.com/getsentry/sentry-go/buffalo"

"sentrybuffaloexample/models"

"github.com/gobuffalo/buffalo-pop/pop/popmw"
contenttype "github.com/gobuffalo/mw-contenttype"
"github.com/gobuffalo/x/sessions"
"github.com/rs/cors"
)

var ENV = envy.Get("GO_ENV", "development")
var app *buffalo.App

func App() *buffalo.App {
if app == nil {
app = buffalo.New(buffalo.Options{
Env: ENV,
SessionStore: sessions.Null{},
PreWares: []buffalo.PreWare{
cors.Default().Handler,
},
SessionName: "_sentrybuffaloexample_session",
})

// To initialize Sentry's handler, you need to initialize Sentry itself beforehand
buildInfo := runtime.Build()
if err := sentry.Init(sentry.ClientOptions{
Release: buildInfo.Version,
Dist: buildInfo.Time.String(),
}); err != nil {
app.Logger().Errorf("Sentry initialization failed: %v\n", err)
}

// Attach the integration as one of your middleware
app.Use(sentrybuffalo.New(sentrybuffalo.Options{
Repanic: true,
}))

// Automatically redirect to SSL
app.Use(forceSSL())

// Log request parameters (filters apply).
app.Use(paramlogger.ParameterLogger)

// Set the request content type to JSON
app.Use(contenttype.Set("application/json"))

// Wraps each request in a transaction.
// c.Value("tx").(*pop.Connection)
// Remove to disable this.
app.Use(popmw.Transaction(models.DB))

app.GET("/", HomeHandler)
}

return app
}
```

## Configuration

`sentrybuffalo` accepts a struct of `Options` that allows you to configure how the handler will behave.

Currently it respects 3 options:

```go
// Repanic configures whether Sentry should repanic after recovery
Repanic bool
// WaitForDelivery indicates whether to wait until panic details have been
// sent to Sentry before panicking or proceeding with a request.
WaitForDelivery bool
// Timeout for the event delivery requests.
Timeout time.Duration
// CaptureError will capture the error if one was returned.
CaptureError bool
```

## Usage

`sentrybuffalo` attaches an instance of `*sentry.Hub` (https://godoc.org/github.com/getsentry/sentry-go#Hub) to the `buffalo.Context`, which makes it available throughout the rest of the request's lifetime.
You can access it by using the `sentrybuffalo.GetHubFromContext()` method on the context itself in any of your proceeding middleware and routes.
And it should be used instead of the global `sentry.CaptureMessage`, `sentry.CaptureException`, or any other calls, as it keeps the separation of data between the requests.

**Keep in mind that `*sentry.Hub` won't be available in middleware attached before to `sentrybuffalo`!**

```go
app = buffalo.New(buffalo.Options{
Env: ENV,
SessionStore: sessions.Null{},
PreWares: []buffalo.PreWare{
cors.Default().Handler,
},
SessionName: "_sentrybuffaloexample_session",
})

// To initialize Sentry's handler, you need to initialize Sentry itself beforehand
buildInfo := runtime.Build()
if err := sentry.Init(sentry.ClientOptions{
Release: buildInfo.Version,
Dist: buildInfo.Time.String(),
}); err != nil {
app.Logger().Errorf("Sentry initialization failed: %v\n", err)
}
app.Use(sentrybuffalo.New(sentrybuffalo.Options{
Repanic: true,
}))

app.Use(func(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {

if hub := sentrybuffalo.GetHubFromContext(c); hub != nil {

if requestIDValue := c.Value("request_id"); requestIDValue != nil {
requestID := requestIDValue.(string)
hub.ConfigureScope(func(scope *sentry.Scope) {
scope.SetExtra("request_id", requestID)
})
}
}

return next(c)
}
}

app.GET("/", func(c buffalo.Context) error {
if hub := sentrybuffalo.GetHubFromContext(c); hub != nil {
hub.WithScope(func(scope *sentry.Scope) {
scope.SetExtra("unwantedQuery", "someQueryDataMaybe")
hub.CaptureMessage("User provided unwanted query string, but we recovered just fine")
})
}
return c.Render(http.StatusOK, r.JSON(map[string]string{"message": "Welcome to Buffalo!"}))
})

app.GET("/foo", func(c buffalo.Context) error {
// sentrybuffalo handler will catch it just fine. Also, because we attached "request_id"
// in the middleware before, it will be sent through as well
panic("y tho")
})

```

### Accessing Request in `BeforeSend` callback

```go
sentry.Init(sentry.ClientOptions{
Dsn: "your-public-dsn",
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
if hint.Context != nil {
if req, ok := hint.Context.Value(sentry.RequestContextKey).(*http.Request); ok {
// You have access to the original Request here
}
}

return event
},
})
```
102 changes: 102 additions & 0 deletions buffalo/sentrybuffalo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package sentrybuffalo

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

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

var sentryHubKey = "sentry_hub"

func SetSentryHubKey(key string) {
sentryHubKey = key
}

type handler struct {
repanic bool
waitForDelivery bool
timeout time.Duration
captureError bool
}

type Options struct {
// Repanic configures whether Sentry should repanic after recovery
Repanic bool
// WaitForDelivery indicates whether to wait until panic details have been
// sent to Sentry before panicking or proceeding with a request.
WaitForDelivery bool
// Timeout for the event delivery requests.
Timeout time.Duration
// CaptureError will capture the error if one was returned.
CaptureError bool
}

// New returns a function that satisfies buffalo.MiddlewareFunc interface
// It can be used with Use() methods.
func New(options Options) buffalo.MiddlewareFunc {
handler := handler{
repanic: false,
timeout: time.Second * 2,
waitForDelivery: false,
captureError: false,
}

if options.Repanic {
handler.repanic = true
}

if options.Timeout != 0 {
handler.timeout = options.Timeout
}

if options.WaitForDelivery {
handler.waitForDelivery = true
}

if options.CaptureError {
handler.captureError = true
}

return handler.handle
}

func (h *handler) handle(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {
r := c.Request()
hub := sentry.CurrentHub().Clone()
hub.Scope().SetRequest(sentry.Request{}.FromHTTPRequest(r))
c.Set(sentryHubKey, hub)
defer h.recoverWithSentry(hub, r)
err := next(c)
if err != nil && h.captureError {
hub.CaptureException(err)
}
return err
}
}

func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) {
if err := recover(); err != nil {
eventID := hub.RecoverWithContext(
context.WithValue(r.Context(), sentry.RequestContextKey, r),
err,
)
if eventID != nil && h.waitForDelivery {
hub.Flush(h.timeout)
}
if h.repanic {
panic(err)
}
}
}

// GetHubFromContext returns sentry.Hub from the buffalo.Context
func GetHubFromContext(c buffalo.Context) *sentry.Hub {
if hub, ok := c.Value(sentryHubKey).(*sentry.Hub); ok {
return hub
}
return nil
}
21 changes: 21 additions & 0 deletions example/buffalo/.buffalo.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
app_root: .
ignored_folders:
- vendor
- log
- logs
- assets
- public
- grifts
- tmp
- bin
- node_modules
- .sass-cache
included_extensions:
- .go
- .env
build_path: tmp
build_delay: 200ns
binary_name: sentrybuffaloexample-build
command_flags: []
enable_colors: true
log_name: buffalo
20 changes: 20 additions & 0 deletions example/buffalo/.codeclimate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
engines:
fixme:
enabled: true
gofmt:
enabled: true
golint:
enabled: true
govet:
enabled: true
exclude_paths:
- grifts/**/*
- "**/*_test.go"
- "*_test.go"
- "**_test.go"
- logs/*
- public/*
- templates/*
ratings:
paths:
- "**.go"
3 changes: 3 additions & 0 deletions example/buffalo/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
*.log
bin/
9 changes: 9 additions & 0 deletions example/buffalo/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This .env file was generated by buffalo, add here the env variables you need
# buffalo to load into the ENV on application startup so your application works correctly.
# To add variables use KEY=VALUE format, you can later retrieve this in your application
# by using os.Getenv("KEY").
#
# Example:
# DATABASE_PASSWORD=XXXXXXXXX
# SESSION_SECRET=XXXXXXXXX
# SMTP_SERVER=XXXXXXXXX
39 changes: 39 additions & 0 deletions example/buffalo/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# This is a multi-stage Dockerfile and requires >= Docker 17.05
# https://docs.docker.com/engine/userguide/eng-image/multistage-build/
FROM gobuffalo/buffalo:v0.16.2 as builder

ENV GO111MODULE on
ENV GOPROXY http://proxy.golang.org

RUN mkdir -p $GOPATH/src/example
WORKDIR $GOPATH/src/example

# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download

ADD . .
RUN buffalo build --static -o /bin/app

FROM alpine
RUN apk add --no-cache bash
RUN apk add --no-cache ca-certificates

WORKDIR /bin/

COPY --from=builder /bin/app .

# Uncomment to run the binary in "production" mode:
# ENV GO_ENV=production

# Bind the app to 0.0.0.0 so it can be seen from outside the container
ENV ADDR=0.0.0.0

EXPOSE 3000

# Uncomment to run the migrations before running the binary:
# CMD /bin/app migrate; /bin/app
CMD exec /bin/app
Loading