Share on Twitter
Share on Facebook
Share on HackerNews

Introducing: Sentry's Unified Go SDK

According to Stack Overflow’s Developer Survey 2019, Go is the third most wanted language to learn, as well as the third-best paid technology in the field. It is not a surprise, as it is one of the languages used for writing critical parts of a lot of large systems. The language design and syntax are simple, but developing in Go is far from easy.

Some of the features Go provides:

  • recovering from panic
  • reporting errors
  • recording breadcrumbs
  • logging messages
  • extracting stack traces from errors and panics
  • errors filtering
  • integration with various HTTP libraries
  • async/sync transports
  • serverless support
  • extracting request/os/device data
  • thread-safe data separation

We decided to spend some time on writing a new, unified SDK, that supports all recent versions of the language, utilizes its features, and gives developers the most helpful hints possible for where and why the error might happen.

Here’s what a Go exception looks like in Sentry. sentry-go-exception

Getting Started

sentry-go provides a Sentry client implementation, which is in-line with our Unified API guidelines and is intended to replace the old raven-go package. The SDK supports Go Modules by design; therefore, when installed, it picks up the latest version of the SDK and stores it inside the go.mod file. Non-major changes should never break your builds.

$ go get github.com/getsentry/sentry-go

The only thing you need to call to initialize the SDK is sentry.Init with a proper DSN obtained from your Sentry account.

sentry.Init(sentry.ClientOptions{ 
	Dsn: "YOUR_PUBLIC_DSN",
})

Once configured, you can start using any publicly available API, and it’ll track your data and report captured errors.

Now, let’s go through some real-world use-cases, as this sounds way more interesting.

Capturing Panics

Sentry provides two ways of recovering from panics: RecoverWithContext(), which can pass Context around, and Recover(), which does not pass Context around. Unless you are moving data around or using some cancel/timeout functionalities, Recover() is the way to go.

Below, we call a function that we know breaks by fetching a number from a slice that is definitely not there. How can we make sure that exception reports to Sentry? With Recover(), that’s how.

func bar() int {
	var luckyNumber []int
	return luckyNumber[42]
}

func foo() int {
	return bar()
}

func main() {
	// Initialize Sentry here

	defer sentry.Flush(time.Second * 5)
	defer sentry.Recover()

	foo()
}

By default, sentry-go uses asynchronous transport, which requires an explicit signal for event delivery finish using the sentry.Flush method. Otherwise, the program doesn’t wait for the async HTTP calls to return a response and exits the process immediately when reaching the end of the main function. sentry.Flush isn’t required inside a running goroutine or if you would use HTTPSyncTransport, which you can read about in our docs.

Capturing Errors

Reporting errors is even easier, because in Go, “error is just a value.”

var ErrMissingLuckyNumber = errors.New("Missing lucky number, sorry")
var luckyNumbers = map[string]int{
	"pickle": 42,
}

func getLuckyNumber(key string) (int, error) {
	if val, ok := luckyNumbers[key]; ok {
		return val, nil
	} else {
		return 0, ErrMissingLuckyNumber
	}
}

func main() {
  // Initialize Sentry here

	number, err := getLuckyNumber("pickle")

	if err != nil {
		sentry.CaptureException(err)
		sentry.Flush(time.Second * 5)
	} else {
		fmt.Println("Your lucky number is:", number)
	}
}

The same goes for simple messages.

// same getLuckyNumber implementation

func main() {
  // Initialize Sentry here

	if _, err := getLuckyNumber("pickle"); err != nil {
		sentry.CaptureMessage("Lucky number wasn't there, but let just log it as a message and proceed")
  }
  
  // the rest of your  program
}

Breadcrumbs are a small message-hints that help track what went wrong during program execution, including some HTTP call, a DB query, data parsing, you name it.

Let’s simulate the scenario:

func performHTTPRequest(url string) string {
	sentry.AddBreadcrumb(&sentry.Breadcrumb{
		Message:  fmt.Sprintf("GET | %s", url),
		Category: "http",
	})

	// performing HTTP request
	return "I'm Pickle Rick!"
}

func performDBQuery(query string) string {
	sentry.AddBreadcrumb(&sentry.Breadcrumb{
		Message:  query,
		Category: "db",
	})

	// performing DB query
	return "https://example/com"
}

func processData(data string) error {
	return errors.New("something broke, sorry")
}

func main() {
  // Initialize Sentry here
  
	url := performDBQuery("Robert;); DROP TABLE Students;--")

	if data := performHTTPRequest(url); data != "" {
		sentry.AddBreadcrumb(&sentry.Breadcrumb{
			Message: data,
		})

		if err := processData(data); err != nil {
			sentry.CaptureException(err)
		}
	}
}

With this setup, your error has three beautiful breadcrumbs that look exactly like the on the screenshot in the very top of this blog post.

Contextual Data

Now and then, it’s useful to correlate some additional data with a captured panic or error. To do this, we also provide two methods: one that stores the data in the global Scope object and attaches it to every following event, and one that does this only for events captured in its callback function.

func main() {
  // This data will be attached to every event sent to Sentry
  sentry.ConfigureScope(func(scope *sentry.Scope) {
		scope.SetTag("isthis", "reallife")
		scope.SetExtra("oristhis", "justfantasy")
		scope.SetUser(sentry.User{
			ID: "1337",
		})
  })
  
  // For example this one
  sentry.CaptureMessage("hey there!")

  // However, this data will be only attached to the events that are captured inside it's callback
  // it'll contain global data configure above and its own "internal" one
  sentry.WithScope(func(scope *sentry.Scope) {
		scope.SetExtra("caught", "inalandslide")
    sentry.CaptureMessage("no escape from reality!")
  })

  // And this one
  sentry.CaptureMessage("hey there too!")
}

Goroutines

Capturing panics and errors, breadcrumbs, and contextual data are all well and good, but what about goroutines? No worries — we’ve got you covered.

The only thing you have to remember is that, in threaded environments, you have to take care of the data that’s living inside it, be it Scope instance or the Client you configure with sentry.Init. Thankfully, Hub keeps track of corresponding Scope and Client, allowing them to communicate with each other.

One of the methods that Hub uses is Clone, which (as you can guess) clones the top-most scope on the stack and reassigns the pre-configured client. Clone enables calls all major public APIs, but in a completely separated manner, where you don’t have to worry about overriding your scope data or race-conditions.

Here’s how:

func main() {
  // Initialize Sentry here

  go func() {
    localHub := sentry.CurrentHub().Clone()
    localHub.ConfigureScope(func(scope *sentry.Scope) {
      scope.SetTag("secretTag", "go#1")
    })
    localHub.CaptureMessage("Hello from Goroutine! #1")
  }()

  go func() {
    localHub := sentry.CurrentHub().Clone()
    localHub.ConfigureScope(func(scope *sentry.Scope) {
      scope.SetTag("secretTag", "go#2")
    })
    localHub.CaptureMessage("Hello from Goroutine! #2")
  }()
}

Hub and Clone make sure that the data belongs where it needs to and that captured events are enhanced in a correct way.

HTTP Packages

Our Go SDK provides integration with a variety of HTTP libraries: net/HTTP, Echo, FastHTTP, Iris, Gin, Martini, Negroni, and the list is still growing. Every integration automatically captures panics for your request handlers and exposes Request object for your disposal.

Let’s see how one may use it with one of the most popular packages, gin:

package main

import (
	"fmt"
	"net/http"

	"github.com/getsentry/sentry-go"
	sentrygin "github.com/getsentry/sentry-go/gin"
	"github.com/gin-gonic/gin"
)

func main() {
	sentry.Init(sentry.ClientOptions{
		Dsn: "YOUR_PUBLIC_DSN",
		AttachStacktrace: true,
	})

	app := gin.Default()

  // Gin has its own Panic handle, which sends 500 to the user
  // therefore after catching and reporting it to Sentry, we hand it over further
	app.Use(sentrygin.New(sentrygin.Options{
		Repanic: true,
	}))

  // Add some contextual data, that can be extracted from the request
	app.Use(func(ctx *gin.Context) {
		if hub := sentrygin.GetHubFromContext(ctx); hub != nil {
			hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt")
		}
		ctx.Next()
	})

	app.GET("/", func(ctx *gin.Context) {
    // Use a Hub that is stored on the request's context (added by Sentry's integration)
    // And capture a message that will inform us about some unusual behavior,
    // despite request not panicking
		if hub := sentrygin.GetHubFromContext(ctx); hub != nil {
			hub.WithScope(func(scope *sentry.Scope) {
				scope.SetExtra("unwantedQuery", "someQueryDataMaybe")
				hub.CaptureMessage("User provided unwanted query string, but we recovered just fine")
			})
    }
    
		ctx.Status(http.StatusOK)
	})

	app.GET("/foo", func(ctx *gin.Context) {
		// sentrygin handler will catch it just fine, and because we attached "someRandomTag"
		// in the middleware before, it will be sent through as well
		panic("y tho")
	})

	_ = app.Run(":3000")

Note the AttackStacktrace client option that ensures, despite panic throwing just a string our way, we can extract the stack trace and add more context to the Sentry event report page.

What’s next?

Right now sentry-go is in it’s “pre-v1” stage. The SDK has a stable API that we test in the wild, but we look for ways to make it better (including listening to feedback). We already have some ideas that you can track in our GitHub issues page.

If you’d like to request a feature, help with implementing a feature, or have a better way of solving problems, feel free to let us know, and we’ll make sure that your voice is heard!

Your code is broken. Let's Fix it.
Get Started