Add support for error (panic) reporting through Sentry
This commit is contained in:
parent
4973638e2a
commit
60ad32e440
13
Gopkg.lock
generated
13
Gopkg.lock
generated
@ -76,6 +76,17 @@
|
||||
pruneopts = "UT"
|
||||
revision = "b57537c92a6b4155c5219ce0d7d2a9c0cc74b1c4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f59f34bb582fbc13885cd0338570bc0f21738fc711065527c36928213de62c1b"
|
||||
name = "github.com/getsentry/sentry-go"
|
||||
packages = [
|
||||
".",
|
||||
"http",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "79cad3fec863752ea61059d4565f85b23da2d9d1"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cb53653e4e410ef91f3a83568464ef966e1732ad9a00b2719571f90eae9a15a7"
|
||||
name = "github.com/go-chi/chi"
|
||||
@ -565,6 +576,8 @@
|
||||
"github.com/dgrijalva/jwt-go",
|
||||
"github.com/disintegration/imaging",
|
||||
"github.com/edwvee/exiffix",
|
||||
"github.com/getsentry/sentry-go",
|
||||
"github.com/getsentry/sentry-go/http",
|
||||
"github.com/go-chi/chi",
|
||||
"github.com/go-chi/chi/middleware",
|
||||
"github.com/go-chi/cors",
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
cfg := system.Configure()
|
||||
cmd := cfg.MakeCLI(cli.Context())
|
||||
cli.HandleError(cmd.Execute())
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
sentry "github.com/getsentry/sentry-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory"
|
||||
"go.uber.org/zap"
|
||||
@ -34,6 +35,8 @@ func TryToConnect(ctx context.Context, log *zap.Logger, name, dsn, profiler stri
|
||||
zap.Duration("timeout", timeout))
|
||||
|
||||
go func() {
|
||||
defer sentry.Recover()
|
||||
|
||||
var (
|
||||
try = 0
|
||||
)
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
sentry "github.com/getsentry/sentry-go"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@ -111,9 +112,10 @@ func (svc *service) Grant(ctx context.Context, wl Whitelist, rules ...*Rule) (er
|
||||
// Watches for changes
|
||||
func (svc service) Watch(ctx context.Context) {
|
||||
go func() {
|
||||
defer sentry.Recover()
|
||||
|
||||
var ticker = time.NewTicker(watchInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
sentry "github.com/getsentry/sentry-go"
|
||||
"github.com/titpetric/factory"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/internal/payload"
|
||||
@ -43,6 +44,8 @@ func EventQueue(origin uint64) *eventQueue {
|
||||
// @todo: retire this function, use Events().Push(ctx, item) directly.
|
||||
func (eq *eventQueue) store(ctx context.Context, qp repository.EventsRepository) {
|
||||
go func() {
|
||||
defer sentry.Recover()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
sentry "github.com/getsentry/sentry-go"
|
||||
"github.com/go-chi/chi"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@ -27,6 +28,8 @@ func Init(ctx context.Context, config *Config) *Websocket {
|
||||
events := repository.Events()
|
||||
|
||||
go func() {
|
||||
defer sentry.Recover()
|
||||
|
||||
for {
|
||||
if err := eq.feedSessions(ctx, events, store); err != nil {
|
||||
if err == context.Canceled {
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
sentry "github.com/getsentry/sentry-go"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
@ -110,6 +111,8 @@ func (sess *Session) connected() (err error) {
|
||||
|
||||
// Create a heartbeat every minute for this user
|
||||
go func() {
|
||||
defer sentry.Recover()
|
||||
|
||||
t := time.NewTicker(time.Second * 60)
|
||||
for {
|
||||
select {
|
||||
|
||||
@ -73,7 +73,6 @@ func LogResponse(next http.Handler) http.Handler {
|
||||
}()
|
||||
|
||||
next.ServeHTTP(wrapped, req)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@ -2,23 +2,47 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"go.uber.org/zap"
|
||||
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
)
|
||||
|
||||
func Base() []func(http.Handler) http.Handler {
|
||||
func Base(log *zap.Logger) []func(http.Handler) http.Handler {
|
||||
return []func(http.Handler) http.Handler{
|
||||
handleCORS,
|
||||
middleware.RealIP,
|
||||
middleware.RequestID,
|
||||
contextLogger(log),
|
||||
}
|
||||
}
|
||||
|
||||
func Logging(log *zap.Logger) []func(http.Handler) http.Handler {
|
||||
return []func(http.Handler) http.Handler{
|
||||
contextLogger(log),
|
||||
LogRequest,
|
||||
LogResponse,
|
||||
}
|
||||
func Sentry() func(http.Handler) http.Handler {
|
||||
return sentryhttp.New(sentryhttp.Options{
|
||||
Repanic: true,
|
||||
}).Handle
|
||||
}
|
||||
|
||||
// HandlePanic sends 500 error when panic occurs inside the request call
|
||||
func HandlePanic(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
w.WriteHeader(500)
|
||||
|
||||
if _, has := os.LookupEnv("DEBUG_DUMP_STACK_IN_RESPONSE"); has {
|
||||
// Provide nice call stack on endpoint when
|
||||
// we crash
|
||||
w.Write(debug.Stack())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
@ -86,12 +86,28 @@ func (s Server) Serve(ctx context.Context) {
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(Base()...)
|
||||
// Base middleware, CORS, RealIP, RequestID, context-logger
|
||||
router.Use(Base(s.log)...)
|
||||
|
||||
if s.httpOpt.Logging {
|
||||
router.Use(Logging(s.log)...)
|
||||
// Logging request if enabled
|
||||
if s.httpOpt.LogRequest {
|
||||
router.Use(LogRequest)
|
||||
}
|
||||
|
||||
// Logging response if enabled
|
||||
if s.httpOpt.LogResponse {
|
||||
router.Use(LogResponse)
|
||||
}
|
||||
|
||||
// Handle panic (sets 500 Server error headers)
|
||||
router.Use(HandlePanic)
|
||||
|
||||
// Reports error to Sentry if enabled
|
||||
if s.httpOpt.EnablePanicReporting {
|
||||
router.Use(Sentry())
|
||||
}
|
||||
|
||||
// Metrics tracking middleware
|
||||
if s.httpOpt.EnableMetrics {
|
||||
router.Use(Middleware(s.httpOpt.MetricsServiceLabel))
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/internal/auth"
|
||||
@ -28,6 +29,29 @@ func InitGeneralServices(logOpt *options.LogOpt, smtpOpt *options.SMTPOpt, jwtOp
|
||||
)
|
||||
}
|
||||
|
||||
func InitSentry(sentryOpt *options.SentryOpt) error {
|
||||
if sentryOpt.DSN == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sentry.Init(sentry.ClientOptions{
|
||||
Dsn: sentryOpt.DSN,
|
||||
Debug: sentryOpt.Debug,
|
||||
AttachStacktrace: sentryOpt.AttachStacktrace,
|
||||
SampleRate: sentryOpt.SampleRate,
|
||||
MaxBreadcrumbs: sentryOpt.MaxBreadcrumbs,
|
||||
IgnoreErrors: nil,
|
||||
BeforeSend: nil,
|
||||
BeforeBreadcrumb: nil,
|
||||
Integrations: nil,
|
||||
Transport: nil,
|
||||
ServerName: sentryOpt.ServerName,
|
||||
Release: sentryOpt.Release,
|
||||
Dist: sentryOpt.Dist,
|
||||
Environment: sentryOpt.Environment,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleError(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
|
||||
@ -50,6 +50,11 @@ func fill(opt interface{}, pfix string) {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Float32 {
|
||||
v.FieldByName(t.Name).SetFloat(float64(EnvFloat32(pfix, tag, float32(f.Float()))))
|
||||
continue
|
||||
}
|
||||
|
||||
panic("unsupported type/kind for field " + t.Name)
|
||||
|
||||
}
|
||||
@ -94,6 +99,17 @@ func EnvInt(pfix, key string, def int) int {
|
||||
return def
|
||||
}
|
||||
|
||||
func EnvFloat32(pfix, key string, def float32) float32 {
|
||||
for _, key = range makeEnvKeys(pfix, key) {
|
||||
if val, has := os.LookupEnv(key); has {
|
||||
if i, err := cast.ToFloat32E(val); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func EnvDuration(pfix, key string, def time.Duration) time.Duration {
|
||||
for _, key = range makeEnvKeys(pfix, key) {
|
||||
if val, has := os.LookupEnv(key); has {
|
||||
|
||||
@ -6,9 +6,10 @@ import (
|
||||
|
||||
type (
|
||||
HTTPOpt struct {
|
||||
Addr string `env:"HTTP_ADDR"`
|
||||
Logging bool `env:"HTTP_LOG_REQUESTS"`
|
||||
Tracing bool `env:"HTTP_ERROR_TRACING"`
|
||||
Addr string `env:"HTTP_ADDR"`
|
||||
LogRequest bool `env:"HTTP_LOG_REQUEST"`
|
||||
LogResponse bool `env:"HTTP_LOG_RESPONSE"`
|
||||
Tracing bool `env:"HTTP_ERROR_TRACING"`
|
||||
|
||||
EnableVersionRoute bool `env:"HTTP_ENABLE_VERSION_ROUTE"`
|
||||
EnableDebugRoute bool `env:"HTTP_ENABLE_DEBUG_ROUTE"`
|
||||
@ -17,13 +18,16 @@ type (
|
||||
MetricsServiceLabel string `env:"HTTP_METRICS_NAME"`
|
||||
MetricsUsername string `env:"HTTP_METRICS_USERNAME"`
|
||||
MetricsPassword string `env:"HTTP_METRICS_PASSWORD"`
|
||||
|
||||
EnablePanicReporting bool `env:"HTTP_REPORT_PANIC"`
|
||||
}
|
||||
)
|
||||
|
||||
func HTTP(pfix string) (o *HTTPOpt) {
|
||||
o = &HTTPOpt{
|
||||
Addr: ":80",
|
||||
Logging: true,
|
||||
LogRequest: false,
|
||||
LogResponse: false,
|
||||
Tracing: false,
|
||||
EnableVersionRoute: true,
|
||||
EnableDebugRoute: false,
|
||||
@ -31,6 +35,9 @@ func HTTP(pfix string) (o *HTTPOpt) {
|
||||
MetricsServiceLabel: "corteza",
|
||||
MetricsUsername: "metrics",
|
||||
|
||||
// Reports panics to Sentry throught HTTP middleware
|
||||
EnablePanicReporting: true,
|
||||
|
||||
// Setting metrics password to random string to prevent security accidents...
|
||||
MetricsPassword: string(rand.Bytes(5)),
|
||||
}
|
||||
|
||||
34
pkg/cli/options/sentry.go
Normal file
34
pkg/cli/options/sentry.go
Normal file
@ -0,0 +1,34 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/internal/version"
|
||||
)
|
||||
|
||||
type (
|
||||
SentryOpt struct {
|
||||
DSN string `env:"SENTRY_DSN"`
|
||||
|
||||
Debug bool `env:"SENTRY_DEBUG"`
|
||||
AttachStacktrace bool `env:"SENTRY_ATTACH_STACKTRACE"`
|
||||
SampleRate float32 `env:"SENTRY_SAMPLE_RATE"`
|
||||
MaxBreadcrumbs int `env:"SENTRY_MAX_BREADCRUMBS"`
|
||||
|
||||
ServerName string `env:"SENTRY_SERVERNAME"`
|
||||
Release string `env:"SENTRY_RELEASE"`
|
||||
Dist string `env:"SENTRY_DIST"`
|
||||
Environment string `env:"SENTRY_ENVIRONMENT"`
|
||||
}
|
||||
)
|
||||
|
||||
func Sentry(pfix string) (o *SentryOpt) {
|
||||
o = &SentryOpt{
|
||||
AttachStacktrace: true,
|
||||
MaxBreadcrumbs: 0,
|
||||
|
||||
Release: version.Version,
|
||||
}
|
||||
|
||||
fill(o, pfix)
|
||||
|
||||
return
|
||||
}
|
||||
@ -3,6 +3,7 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -48,6 +49,7 @@ type (
|
||||
HttpClientOpt *options.HttpClientOpt
|
||||
DbOpt *options.DBOpt
|
||||
ProvisionOpt *options.ProvisionOpt
|
||||
SentryOpt *options.SentryOpt
|
||||
|
||||
// DB Connection name, defaults to ServiceName
|
||||
DatabaseName string
|
||||
@ -179,6 +181,7 @@ func (c *Config) Init() {
|
||||
c.HttpClientOpt = options.HttpClient(c.EnvPrefix)
|
||||
c.DbOpt = options.DB(c.ServiceName)
|
||||
c.ProvisionOpt = options.Provision(c.ServiceName)
|
||||
c.SentryOpt = options.Sentry(c.EnvPrefix)
|
||||
|
||||
if c.RootCommandDBSetup == nil {
|
||||
c.RootCommandDBSetup = Runners{func(ctx context.Context, cmd *cobra.Command, c *Config) (err error) {
|
||||
@ -213,6 +216,11 @@ func (c *Config) MakeCLI(ctx context.Context) (cmd *cobra.Command) {
|
||||
Use: c.RootCommandName,
|
||||
TraverseChildren: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
if err = InitSentry(c.SentryOpt); err != nil {
|
||||
c.Log.Error("could not initialize Sentry", zap.Error(err))
|
||||
}
|
||||
|
||||
defer sentry.Recover()
|
||||
InitGeneralServices(c.LogOpt, c.SmtpOpt, c.JwtOpt, c.HttpClientOpt)
|
||||
|
||||
err = c.RootCommandDBSetup.Run(ctx, cmd, c)
|
||||
@ -232,6 +240,7 @@ func (c *Config) MakeCLI(ctx context.Context) (cmd *cobra.Command) {
|
||||
}
|
||||
|
||||
serveApiCmd := c.ApiServer.Command(ctx, c.ApiServerCommandName, c.EnvPrefix, func(ctx context.Context) (err error) {
|
||||
defer sentry.Recover()
|
||||
return c.ApiServerPreRun.Run(ctx, cmd, c)
|
||||
})
|
||||
|
||||
|
||||
@ -49,7 +49,6 @@ func Configure() *cli.Config {
|
||||
ApiServerPreRun: cli.Runners{
|
||||
func(ctx context.Context, cmd *cobra.Command, c *cli.Config) error {
|
||||
external.Init(service.DefaultIntSettings)
|
||||
|
||||
go service.Watchers(ctx)
|
||||
return nil
|
||||
},
|
||||
|
||||
12
vendor/github.com/getsentry/sentry-go/.craft.yml
generated
vendored
Normal file
12
vendor/github.com/getsentry/sentry-go/.craft.yml
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
github:
|
||||
owner: getsentry
|
||||
repo: sentry-go
|
||||
preReleaseCommand: bash scripts/craft-pre-release.sh
|
||||
changelogPolicy: simple
|
||||
targets:
|
||||
- name: github
|
||||
tagPrefix: v
|
||||
- name: registry
|
||||
type: sdk
|
||||
config:
|
||||
canonical: "github:getsentry/sentry-go"
|
||||
6
vendor/github.com/getsentry/sentry-go/.gitignore
generated
vendored
Normal file
6
vendor/github.com/getsentry/sentry-go/.gitignore
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
coverage.txt
|
||||
|
||||
# Just my personal way of tracking stuff — Kamil
|
||||
FIXME.md
|
||||
TODO.md
|
||||
!NOTES.md
|
||||
9
vendor/github.com/getsentry/sentry-go/.golangci.yml
generated
vendored
Normal file
9
vendor/github.com/getsentry/sentry-go/.golangci.yml
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- megacheck
|
||||
- stylecheck
|
||||
run:
|
||||
skip-dirs:
|
||||
- echo
|
||||
- example/echo
|
||||
28
vendor/github.com/getsentry/sentry-go/.travis.yml
generated
vendored
Normal file
28
vendor/github.com/getsentry/sentry-go/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
sudo: false
|
||||
|
||||
language: go
|
||||
go:
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
before_install:
|
||||
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.15.0
|
||||
|
||||
script:
|
||||
- golangci-lint run
|
||||
- go build
|
||||
- go test
|
||||
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
- https://zeus.ci/hooks/befe9810-9285-11e9-b01a-0a580a281808/public/provider/travis/webhook
|
||||
on_success: always
|
||||
on_failure: always
|
||||
on_start: always
|
||||
on_cancel: always
|
||||
on_error: always
|
||||
64
vendor/github.com/getsentry/sentry-go/CHANGELOG.md
generated
vendored
Normal file
64
vendor/github.com/getsentry/sentry-go/CHANGELOG.md
generated
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
# Changelog
|
||||
|
||||
## v0.1.1
|
||||
|
||||
- fix: Check for initialized Client in AddBreadcrumbs (#20)
|
||||
- build: Bump version when releasing with Craft (#19)
|
||||
|
||||
## v0.1.0
|
||||
|
||||
- First stable release! \o/
|
||||
|
||||
## v0.0.1-beta.5
|
||||
|
||||
- feat: **[breaking]** Add `NewHTTPTransport` and `NewHTTPSyncTransport` which accepts all transport options
|
||||
- feat: New `HTTPSyncTransport` that blocks after each call
|
||||
- feat: New `Echo` integration
|
||||
- ref: **[breaking]** Remove `BufferSize` option from `ClientOptions` and move it to `HTTPTransport` instead
|
||||
- ref: Export default `HTTPTransport`
|
||||
- ref: Export `net/http` integration handler
|
||||
- ref: Set `Request` instantly in the package handlers, not in `recoverWithSentry` so it can be accessed later on
|
||||
- ci: Add craft config
|
||||
|
||||
## v0.0.1-beta.4
|
||||
|
||||
- feat: `IgnoreErrors` client option and corresponding integration
|
||||
- ref: Reworked `net/http` integration, wrote better example and complete readme
|
||||
- ref: Reworked `Gin` integration, wrote better example and complete readme
|
||||
- ref: Reworked `Iris` integration, wrote better example and complete readme
|
||||
- ref: Reworked `Negroni` integration, wrote better example and complete readme
|
||||
- ref: Reworked `Martini` integration, wrote better example and complete readme
|
||||
- ref: Remove `Handle()` from frameworks handlers and return it directly from New
|
||||
|
||||
## v0.0.1-beta.3
|
||||
|
||||
- feat: `Iris` framework support with `sentryiris` package
|
||||
- feat: `Gin` framework support with `sentrygin` package
|
||||
- feat: `Martini` framework support with `sentrymartini` package
|
||||
- feat: `Negroni` framework support with `sentrynegroni` package
|
||||
- feat: Add `Hub.Clone()` for easier frameworks integration
|
||||
- feat: Return `EventID` from `Recovery` methods
|
||||
- feat: Add `NewScope` and `NewEvent` functions and use them in the whole codebase
|
||||
- feat: Add `AddEventProcessor` to the `Client`
|
||||
- fix: Operate on requests body copy instead of the original
|
||||
- ref: Try to read source files from the root directory, based on the filename as well, to make it work on AWS Lambda
|
||||
- ref: Remove `gocertifi` dependence and document how to provide your own certificates
|
||||
- ref: **[breaking]** Remove `Decorate` and `DecorateFunc` methods in favor of `sentryhttp` package
|
||||
- ref: **[breaking]** Allow for integrations to live on the client, by passing client instance in `SetupOnce` method
|
||||
- ref: **[breaking]** Remove `GetIntegration` from the `Hub`
|
||||
- ref: **[breaking]** Remove `GlobalEventProcessors` getter from the public API
|
||||
|
||||
## v0.0.1-beta.2
|
||||
|
||||
- feat: Add `AttachStacktrace` client option to include stacktrace for messages
|
||||
- feat: Add `BufferSize` client option to configure transport buffer size
|
||||
- feat: Add `SetRequest` method on a `Scope` to control `Request` context data
|
||||
- feat: Add `FromHTTPRequest` for `Request` type for easier extraction
|
||||
- ref: Extract `Request` information more accurately
|
||||
- fix: Attach `ServerName`, `Release`, `Dist`, `Environment` options to the event
|
||||
- fix: Don't log events dropped due to full transport buffer as sent
|
||||
- fix: Don't panic and create an appropriate event when called `CaptureException` or `Recover` with `nil` value
|
||||
|
||||
## v0.0.1-beta
|
||||
|
||||
- Initial release
|
||||
41
vendor/github.com/getsentry/sentry-go/CONTRIBUTION.md
generated
vendored
Normal file
41
vendor/github.com/getsentry/sentry-go/CONTRIBUTION.md
generated
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
$ go test
|
||||
```
|
||||
|
||||
### Watch mode
|
||||
|
||||
Use: https://github.com/cespare/reflex
|
||||
|
||||
```bash
|
||||
$ reflex -g '*.go' -d "none" -- sh -c 'printf "\n"; go test'
|
||||
```
|
||||
|
||||
### With data race detection
|
||||
|
||||
```bash
|
||||
$ go test -race
|
||||
```
|
||||
|
||||
### Coverage
|
||||
```bash
|
||||
$ go test -race -coverprofile=coverage.txt -covermode=atomic && go tool cover -html coverage.txt
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
```bash
|
||||
$ golangci-lint run
|
||||
```
|
||||
|
||||
## Release
|
||||
|
||||
1. Update changelog with new version in `vX.X.X` format title and list of changes
|
||||
2. Commit with `release: X.X.X` commit message and push to `master`
|
||||
3. Let `craft` do the rest
|
||||
|
||||
```bash
|
||||
$ craft prepare X.X.X
|
||||
$ craft publish X.X.X --skip-status-check
|
||||
```
|
||||
9
vendor/github.com/getsentry/sentry-go/LICENSE
generated
vendored
Normal file
9
vendor/github.com/getsentry/sentry-go/LICENSE
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
392
vendor/github.com/getsentry/sentry-go/MIGRATION.md
generated
vendored
Normal file
392
vendor/github.com/getsentry/sentry-go/MIGRATION.md
generated
vendored
Normal file
@ -0,0 +1,392 @@
|
||||
# `raven-go` to `sentry-go` Migration Guide
|
||||
|
||||
## Installation
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
go get github.com/getsentry/raven-go
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
go get github.com/getsentry/sentry-go@v0.0.1
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
import "github.com/getsentry/raven-go"
|
||||
|
||||
func main() {
|
||||
raven.SetDSN("https://16427b2f210046b585ee51fd8a1ac54f@sentry.io/1")
|
||||
}
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: "https://16427b2f210046b585ee51fd8a1ac54f@sentry.io/1",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Sentry initialization failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
SetDSN()
|
||||
SetDefaultLoggerName()
|
||||
SetDebug()
|
||||
SetEnvironment()
|
||||
SetRelease()
|
||||
SetSampleRate()
|
||||
SetIgnoreErrors()
|
||||
SetIncludePaths()
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
sentry.Init(sentry.ClientOptions{
|
||||
Dsn: "https://16427b2f210046b585ee51fd8a1ac54f@sentry.io/1",
|
||||
DebugWriter: os.Stderr,
|
||||
Debug: true,
|
||||
Environment: "environment",
|
||||
Release: "release",
|
||||
SampleRate: 0.5,
|
||||
// IgnoreErrors: TBD,
|
||||
// IncludePaths: TBD
|
||||
})
|
||||
```
|
||||
|
||||
Available options: see [Configuration](https://docs.sentry.io/platforms/go/config/) section.
|
||||
|
||||
### Providing SSL Certificates
|
||||
|
||||
By default, TLS uses the host's root CA set. If you don't have `ca-certificates` (which should be your go-to way of fixing the issue of missing ceritificates) and want to use `gocertifi` instead, you can provide pre-loaded cert files as one of the options to the `sentry.Init` call:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/certifi/gocertifi"
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
sentryClientOptions := sentry.ClientOptions{
|
||||
Dsn: "https://16427b2f210046b585ee51fd8a1ac54f@sentry.io/1",
|
||||
}
|
||||
|
||||
rootCAs, err := gocertifi.CACerts()
|
||||
if err != nil {
|
||||
log.Println("Coudnt load CA Certificates: %v\n", err)
|
||||
} else {
|
||||
sentryClientOptions.CaCerts = rootCAs
|
||||
}
|
||||
|
||||
sentry.Init(sentryClientOptions)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Capturing Errors
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
f, err := os.Open("filename.ext")
|
||||
if err != nil {
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
f, err := os.Open("filename.ext")
|
||||
if err != nil {
|
||||
sentry.CaptureException(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Capturing Panics
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
raven.CapturePanic(func() {
|
||||
// do all of the scary things here
|
||||
}, nil)
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
func() {
|
||||
defer sentry.Recover()
|
||||
// do all of the scary things here
|
||||
}()
|
||||
```
|
||||
|
||||
### Capturing Messages
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
raven.CaptureMessage("Something bad happened and I would like to know about that")
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
sentry.CaptureMessage("Something bad happened and I would like to know about that")
|
||||
```
|
||||
|
||||
### Capturing Events
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
packet := &raven.Packet{
|
||||
Message: "Hand-crafted event",
|
||||
Extra: &raven.Extra{
|
||||
"runtime.Version": runtime.Version(),
|
||||
"runtime.NumCPU": runtime.NumCPU(),
|
||||
},
|
||||
}
|
||||
raven.Capture(packet)
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
event := &sentry.NewEvent()
|
||||
event.Message = "Hand-crafted event"
|
||||
event.Extra["runtime.Version"] = runtime.Version()
|
||||
event.Extra["runtime.NumCPU"] = runtime.NumCPU()
|
||||
|
||||
sentry.CaptureEvent(event)
|
||||
```
|
||||
|
||||
### Additional Data
|
||||
|
||||
See Context section.
|
||||
|
||||
### Event Sampling
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
raven.SetSampleRate(0.25)
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
sentry.Init(sentry.ClientOptions{
|
||||
SampleRate: 0.25,
|
||||
})
|
||||
```
|
||||
|
||||
### Awaiting the response (not recommended)
|
||||
|
||||
```go
|
||||
raven.CaptureMessageAndWait("Something bad happened and I would like to know about that")
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
sentry.CaptureMessage("Something bad happened and I would like to know about that")
|
||||
|
||||
if sentry.Flush(time.Second * 2) {
|
||||
// event delivered
|
||||
} else {
|
||||
// timeout reached
|
||||
}
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
### Per-event
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
raven.CaptureError(err, map[string]string{"browser": "Firefox"}, &raven.Http{
|
||||
Method: "GET",
|
||||
URL: "https://example.com/raven-go"
|
||||
})
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
sentry.WithScope(func(scope *sentry.Scope) {
|
||||
scope.SetTag("browser", "Firefox")
|
||||
scope.SetContext("Request", map[string]string{
|
||||
"Method": "GET",
|
||||
"URL": "https://example.com/raven-go",
|
||||
})
|
||||
sentry.CaptureException(err)
|
||||
})
|
||||
```
|
||||
|
||||
### Globally
|
||||
|
||||
#### SetHttpContext
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
raven.SetHttpContext(&raven.Http{
|
||||
Method: "GET",
|
||||
URL: "https://example.com/raven-go",
|
||||
})
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetContext("Request", map[string]string{
|
||||
"Method": "GET",
|
||||
"URL": "https://example.com/raven-go",
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### SetTagsContext
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
t := map[string]string{"day": "Friday", "sport": "Weightlifting"}
|
||||
raven.SetTagsContext(map[string]string{"day": "Friday", "sport": "Weightlifting"})
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetTags(map[string]string{"day": "Friday", "sport": "Weightlifting"})
|
||||
})
|
||||
```
|
||||
|
||||
#### SetUserContext
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
raven.SetUserContext(&raven.User{
|
||||
ID: "1337",
|
||||
Username: "kamilogorek",
|
||||
Email: "kamil@sentry.io",
|
||||
IP: "127.0.0.1",
|
||||
})
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetUser(sentry.User{
|
||||
ID: "1337",
|
||||
Username: "kamilogorek",
|
||||
Email: "kamil@sentry.io",
|
||||
IPAddress: "127.0.0.1",
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### ClearContext
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
raven.ClearContext()
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.Clear()
|
||||
})
|
||||
```
|
||||
|
||||
#### WrapWithExtra
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
path := "filename.ext"
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
err = raven.WrapWithExtra(err, map[string]string{"path": path, "cwd": os.Getwd()}
|
||||
raven.CaptureError(err, nil)
|
||||
}
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
// use `sentry.WithScope`, see "Context / Per-event Section"
|
||||
path := "filename.ext"
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
sentry.WithScope(func(scope *sentry.Scope) {
|
||||
sentry.SetExtras(map[string]interface{}{"path": path, "cwd": os.Getwd())
|
||||
sentry.CaptureException(err)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Integrations
|
||||
|
||||
### net/http
|
||||
|
||||
raven-go
|
||||
|
||||
```go
|
||||
mux := http.NewServeMux
|
||||
http.Handle("/", raven.Recoverer(mux))
|
||||
|
||||
// or
|
||||
|
||||
func root(w http.ResponseWriter, r *http.Request) {}
|
||||
http.HandleFunc("/", raven.RecoveryHandler(root))
|
||||
```
|
||||
|
||||
sentry-go
|
||||
|
||||
```go
|
||||
sentryHandler := sentryhttp.New(sentryhttp.Options{
|
||||
Repanic: false,
|
||||
WaitForDelivery: true,
|
||||
})
|
||||
|
||||
mux := http.NewServeMux
|
||||
http.Handle("/", sentryHandler.Handle(mux))
|
||||
|
||||
// or
|
||||
|
||||
func root(w http.ResponseWriter, r *http.Request) {}
|
||||
http.HandleFunc("/", sentryHandler.HandleFunc(root))
|
||||
```
|
||||
107
vendor/github.com/getsentry/sentry-go/README.md
generated
vendored
Normal file
107
vendor/github.com/getsentry/sentry-go/README.md
generated
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
<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 SDK for Go
|
||||
|
||||
[](https://travis-ci.com/getsentry/sentry-go)
|
||||
[](https://goreportcard.com/report/github.com/getsentry/sentry-go)
|
||||
|
||||
`sentry-go` provides a Sentry client implementation for the Go programming language. This is the next line of the Go SDK for [Sentry](https://sentry.io/), intended to replace the `raven-go` package.
|
||||
|
||||
> Looking for the old `raven-go` SDK documentation? See the Legacy client section [here](https://docs.sentry.io/clients/go/).
|
||||
> If you want to start using sentry-go instead, check out the [migration guide](https://docs.sentry.io/platforms/go/migration/).
|
||||
|
||||
## Requirements
|
||||
|
||||
We verify this package against N-2 recent versions of Go compiler. As of June 2019, those versions are:
|
||||
|
||||
* 1.10
|
||||
* 1.11
|
||||
* 1.12
|
||||
|
||||
## Installation
|
||||
|
||||
`sentry-go` can be installed like any other Go library through `go get`:
|
||||
|
||||
```bash
|
||||
$ go get github.com/getsentry/sentry-go
|
||||
```
|
||||
|
||||
Or, if you are already using Go Modules, specify a version number as well:
|
||||
|
||||
```bash
|
||||
$ go get github.com/getsentry/sentry-go@v0.1.0
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
To use `sentry-go`, you’ll need to import the `sentry-go` package and initialize it with the client options that will include your DSN. If you specify the `SENTRY_DSN` environment variable, you can omit this value from options and it will be picked up automatically for you. The release and environment can also be specified in the environment variables `SENTRY_RELEASE` and `SENTRY_ENVIRONMENT` respectively.
|
||||
|
||||
More on this in [Configuration](https://docs.sentry.io/platforms/go/config/) section.
|
||||
|
||||
## Usage
|
||||
|
||||
By default, Sentry Go SDK uses asynchronous transport, which in the code example below requires an explicit awaiting for event delivery to be finished using `sentry.Flush` method. It is necessary, because otherwise the program would not wait for the async HTTP calls to return a response, and exit the process immediately when it reached the end of the `main` function. It would not be required inside a running goroutine or if you would use `HTTPSyncTransport`, which you can read about in `Transports` section.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: "___DSN___",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Sentry initialization failed: %v\n", err)
|
||||
}
|
||||
|
||||
f, err := os.Open("filename.ext")
|
||||
if err != nil {
|
||||
sentry.CaptureException(err)
|
||||
sentry.Flush(time.Second * 5)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For more detailed information about how to get the most out of `sentry-go` there is additional documentation available:
|
||||
|
||||
- [Configuration](https://docs.sentry.io/platforms/go/config)
|
||||
- [Error Reporting](https://docs.sentry.io/error-reporting/quickstart?platform=go)
|
||||
- [Enriching Error Data](https://docs.sentry.io/enriching-error-data/context?platform=go)
|
||||
- [Transports](https://docs.sentry.io/platforms/go/transports)
|
||||
- [Integrations](https://docs.sentry.io/platforms/go/integrations)
|
||||
- [net/http](https://docs.sentry.io/platforms/go/http)
|
||||
- [echo](https://docs.sentry.io/platforms/go/echo)
|
||||
- [gin](https://docs.sentry.io/platforms/go/gin)
|
||||
- [iris](https://docs.sentry.io/platforms/go/iris)
|
||||
- [martini](https://docs.sentry.io/platforms/go/martini)
|
||||
- [negroni](https://docs.sentry.io/platforms/go/negroni)
|
||||
|
||||
## Resources:
|
||||
|
||||
- [Bug Tracker](https://github.com/getsentry/sentry-go/issues)
|
||||
- [GitHub Project](https://github.com/getsentry/sentry-go)
|
||||
- [Godocs](https://godoc.org/github.com/getsentry/sentry-go)
|
||||
- [@getsentry](https://twitter.com/getsentry) on Twitter for updates
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the BSD license, see `LICENSE`
|
||||
|
||||
## Community
|
||||
|
||||
Want to join our Sentry's `community-golang` channel, get involved and help us improve the SDK?
|
||||
|
||||
Do not hesistate to shoot me up an email at [kamil@sentry.io](mailto:kamil@sentry.io) for Slack invite!
|
||||
425
vendor/github.com/getsentry/sentry-go/client.go
generated
vendored
Normal file
425
vendor/github.com/getsentry/sentry-go/client.go
generated
vendored
Normal file
@ -0,0 +1,425 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Logger is an instance of log.Logger that is use to provide debug information about running Sentry Client
|
||||
// can be enabled by either using `Logger.SetOutput` directly or with `Debug` client option
|
||||
var Logger = log.New(ioutil.Discard, "[Sentry] ", log.LstdFlags) // nolint: gochecknoglobals
|
||||
|
||||
type EventProcessor func(event *Event, hint *EventHint) *Event
|
||||
|
||||
type EventModifier interface {
|
||||
ApplyToEvent(event *Event, hint *EventHint) *Event
|
||||
}
|
||||
|
||||
var globalEventProcessors []EventProcessor // nolint: gochecknoglobals
|
||||
|
||||
func AddGlobalEventProcessor(processor EventProcessor) {
|
||||
globalEventProcessors = append(globalEventProcessors, processor)
|
||||
}
|
||||
|
||||
// Integration allows for registering a functions that modify or discard captured events.
|
||||
type Integration interface {
|
||||
Name() string
|
||||
SetupOnce(client *Client)
|
||||
}
|
||||
|
||||
// ClientOptions that configures a SDK Client
|
||||
type ClientOptions struct {
|
||||
// The DSN to use. If the DSN is not set, the client is effectively disabled.
|
||||
Dsn string
|
||||
// In debug mode, the debug information is printed to stdout to help you understand what
|
||||
// sentry is doing.
|
||||
Debug bool
|
||||
// Configures whether SDK should generate and attach stacktraces to pure capture message calls.
|
||||
AttachStacktrace bool
|
||||
// The sample rate for event submission (0.0 - 1.0, defaults to 1.0).
|
||||
SampleRate float32
|
||||
// List of regexp strings that will be used to match against event's message
|
||||
// and if applicable, caught errors type and value.
|
||||
// If the match is found, then a whole event will be dropped.
|
||||
IgnoreErrors []string
|
||||
// Before send callback.
|
||||
BeforeSend func(event *Event, hint *EventHint) *Event
|
||||
// Before breadcrumb add callback.
|
||||
BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb
|
||||
// Integrations to be installed on the current Client, receives default integrations
|
||||
Integrations func([]Integration) []Integration
|
||||
// io.Writer implementation that should be used with the `Debug` mode
|
||||
DebugWriter io.Writer
|
||||
// The transport to use.
|
||||
// This is an instance of a struct implementing `Transport` interface.
|
||||
// Defaults to `httpTransport` from `transport.go`
|
||||
Transport Transport
|
||||
// The server name to be reported.
|
||||
ServerName string
|
||||
// The release to be sent with events.
|
||||
Release string
|
||||
// The dist to be sent with events.
|
||||
Dist string
|
||||
// The environment to be sent with events.
|
||||
Environment string
|
||||
// Maximum number of breadcrumbs.
|
||||
MaxBreadcrumbs int
|
||||
// An optional pointer to `http.Transport` that will be used with a default HTTPTransport.
|
||||
HTTPTransport *http.Transport
|
||||
// An optional HTTP proxy to use.
|
||||
// This will default to the `http_proxy` environment variable.
|
||||
// or `https_proxy` if that one exists.
|
||||
HTTPProxy string
|
||||
// An optional HTTPS proxy to use.
|
||||
// This will default to the `HTTPS_PROXY` environment variable
|
||||
// or `http_proxy` if that one exists.
|
||||
HTTPSProxy string
|
||||
// An optionsl CaCerts to use.
|
||||
// Defaults to `gocertifi.CACerts()`.
|
||||
CaCerts *x509.CertPool
|
||||
}
|
||||
|
||||
// Client is the underlying processor that's used by the main API and `Hub` instances.
|
||||
type Client struct {
|
||||
options ClientOptions
|
||||
dsn *Dsn
|
||||
eventProcessors []EventProcessor
|
||||
integrations []Integration
|
||||
Transport Transport
|
||||
}
|
||||
|
||||
// NewClient creates and returns an instance of `Client` configured using `ClientOptions`.
|
||||
func NewClient(options ClientOptions) (*Client, error) {
|
||||
if options.Debug {
|
||||
debugWriter := options.DebugWriter
|
||||
if debugWriter == nil {
|
||||
debugWriter = os.Stdout
|
||||
}
|
||||
Logger.SetOutput(debugWriter)
|
||||
}
|
||||
|
||||
if options.Dsn == "" {
|
||||
options.Dsn = os.Getenv("SENTRY_DSN")
|
||||
}
|
||||
|
||||
if options.Release == "" {
|
||||
options.Release = os.Getenv("SENTRY_RELEASE")
|
||||
}
|
||||
|
||||
if options.Environment == "" {
|
||||
options.Environment = os.Getenv("SENTRY_ENVIRONMENT")
|
||||
}
|
||||
|
||||
dsn, err := NewDsn(options.Dsn)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dsn == nil {
|
||||
Logger.Println("Sentry client initialized with an empty DSN")
|
||||
}
|
||||
|
||||
client := Client{
|
||||
options: options,
|
||||
dsn: dsn,
|
||||
}
|
||||
|
||||
client.setupTransport()
|
||||
client.setupIntegrations()
|
||||
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func (client *Client) setupTransport() {
|
||||
transport := client.options.Transport
|
||||
|
||||
if transport == nil {
|
||||
transport = NewHTTPTransport()
|
||||
}
|
||||
|
||||
transport.Configure(client.options)
|
||||
client.Transport = transport
|
||||
}
|
||||
|
||||
func (client *Client) setupIntegrations() {
|
||||
integrations := []Integration{
|
||||
new(environmentIntegration),
|
||||
new(modulesIntegration),
|
||||
new(ignoreErrorsIntegration),
|
||||
}
|
||||
|
||||
if client.options.Integrations != nil {
|
||||
integrations = client.options.Integrations(integrations)
|
||||
}
|
||||
|
||||
for _, integration := range integrations {
|
||||
if client.integrationAlreadyInstalled(integration.Name()) {
|
||||
Logger.Printf("Integration %s is already installed\n", integration.Name())
|
||||
continue
|
||||
}
|
||||
client.integrations = append(client.integrations, integration)
|
||||
integration.SetupOnce(client)
|
||||
Logger.Printf("Integration installed: %s\n", integration.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// AddEventProcessor adds an event processor to the client.
|
||||
func (client *Client) AddEventProcessor(processor EventProcessor) {
|
||||
client.eventProcessors = append(client.eventProcessors, processor)
|
||||
}
|
||||
|
||||
// Options return `ClientOptions` for the current `Client`.
|
||||
func (client Client) Options() ClientOptions {
|
||||
return client.options
|
||||
}
|
||||
|
||||
// CaptureMessage captures an arbitrary message.
|
||||
func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
|
||||
event := client.eventFromMessage(message, LevelInfo)
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
// CaptureException captures an error.
|
||||
func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
|
||||
event := client.eventFromException(exception, LevelError)
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
// CaptureEvent captures an event on the currently active client if any.
|
||||
//
|
||||
// The event must already be assembled. Typically code would instead use
|
||||
// the utility methods like `CaptureException`. The return value is the
|
||||
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
|
||||
func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
|
||||
return client.processEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
// Recover captures a panic.
|
||||
// Returns `EventID` if successfully, or `nil` if there's no error to recover from.
|
||||
func (client *Client) Recover(err interface{}, hint *EventHint, scope EventModifier) *EventID {
|
||||
if err == nil {
|
||||
err = recover()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err, ok := err.(error); ok {
|
||||
event := client.eventFromException(err, LevelFatal)
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
if err, ok := err.(string); ok {
|
||||
event := client.eventFromMessage(err, LevelFatal)
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recover captures a panic and passes relevant context object.
|
||||
// Returns `EventID` if successfully, or `nil` if there's no error to recover from.
|
||||
func (client *Client) RecoverWithContext(
|
||||
ctx context.Context,
|
||||
err interface{},
|
||||
hint *EventHint,
|
||||
scope EventModifier,
|
||||
) *EventID {
|
||||
if err == nil {
|
||||
err = recover()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if hint.Context == nil && ctx != nil {
|
||||
hint.Context = ctx
|
||||
}
|
||||
|
||||
if err, ok := err.(error); ok {
|
||||
event := client.eventFromException(err, LevelFatal)
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
|
||||
if err, ok := err.(string); ok {
|
||||
event := client.eventFromMessage(err, LevelFatal)
|
||||
return client.CaptureEvent(event, hint, scope)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush notifies when all the buffered events have been sent by returning `true`
|
||||
// or `false` if timeout was reached. It calls `Flush` method of the configured `Transport`.
|
||||
func (client *Client) Flush(timeout time.Duration) bool {
|
||||
return client.Transport.Flush(timeout)
|
||||
}
|
||||
|
||||
func (client *Client) eventFromMessage(message string, level Level) *Event {
|
||||
event := NewEvent()
|
||||
event.Level = level
|
||||
event.Message = message
|
||||
|
||||
if client.Options().AttachStacktrace {
|
||||
event.Threads = []Thread{{
|
||||
Stacktrace: NewStacktrace(),
|
||||
Crashed: false,
|
||||
Current: true,
|
||||
}}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (client *Client) eventFromException(exception error, level Level) *Event {
|
||||
if exception == nil {
|
||||
event := NewEvent()
|
||||
event.Level = level
|
||||
event.Message = fmt.Sprintf("Called %s with nil value", callerFunctionName())
|
||||
return event
|
||||
}
|
||||
|
||||
stacktrace := ExtractStacktrace(exception)
|
||||
|
||||
if stacktrace == nil {
|
||||
stacktrace = NewStacktrace()
|
||||
}
|
||||
|
||||
event := NewEvent()
|
||||
event.Level = level
|
||||
event.Exception = []Exception{{
|
||||
Value: exception.Error(),
|
||||
Type: reflect.TypeOf(exception).String(),
|
||||
Stacktrace: stacktrace,
|
||||
}}
|
||||
return event
|
||||
}
|
||||
|
||||
func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
|
||||
options := client.Options()
|
||||
|
||||
// TODO: Reconsider if its worth going away from default implementation
|
||||
// of other SDKs. In Go zero value (default) for float32 is 0.0,
|
||||
// which means that if someone uses ClientOptions{} struct directly
|
||||
// and we would not check for 0 here, we'd skip all events by default
|
||||
if options.SampleRate != 0.0 {
|
||||
randomFloat := rand.New(rand.NewSource(time.Now().UnixNano())).Float32()
|
||||
if randomFloat > options.SampleRate {
|
||||
Logger.Println("Event dropped due to SampleRate hit")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if event = client.prepareEvent(event, hint, scope); event == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if options.BeforeSend != nil {
|
||||
h := &EventHint{}
|
||||
if hint != nil {
|
||||
h = hint
|
||||
}
|
||||
if event = options.BeforeSend(event, h); event == nil {
|
||||
Logger.Println("Event dropped due to BeforeSend callback")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
client.Transport.SendEvent(event)
|
||||
|
||||
return &event.EventID
|
||||
}
|
||||
|
||||
func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
|
||||
if event.EventID == "" {
|
||||
event.EventID = EventID(uuid())
|
||||
}
|
||||
|
||||
if event.Timestamp == 0 {
|
||||
event.Timestamp = time.Now().Unix()
|
||||
}
|
||||
|
||||
if event.Level == "" {
|
||||
event.Level = LevelInfo
|
||||
}
|
||||
|
||||
if event.ServerName == "" {
|
||||
if client.Options().ServerName != "" {
|
||||
event.ServerName = client.Options().ServerName
|
||||
} else if hostname, err := os.Hostname(); err == nil {
|
||||
event.ServerName = hostname
|
||||
}
|
||||
}
|
||||
|
||||
if event.Release == "" && client.Options().Release != "" {
|
||||
event.Release = client.Options().Release
|
||||
}
|
||||
|
||||
if event.Dist == "" && client.Options().Dist != "" {
|
||||
event.Dist = client.Options().Dist
|
||||
}
|
||||
|
||||
if event.Environment == "" && client.Options().Environment != "" {
|
||||
event.Environment = client.Options().Environment
|
||||
}
|
||||
|
||||
event.Platform = "go"
|
||||
event.Sdk = SdkInfo{
|
||||
Name: "sentry.go",
|
||||
Version: Version,
|
||||
Integrations: client.listIntegrations(),
|
||||
Packages: []SdkPackage{{
|
||||
Name: "sentry-go",
|
||||
Version: Version,
|
||||
}},
|
||||
}
|
||||
|
||||
event = scope.ApplyToEvent(event, hint)
|
||||
|
||||
for _, processor := range client.eventProcessors {
|
||||
id := event.EventID
|
||||
event = processor(event, hint)
|
||||
if event == nil {
|
||||
Logger.Printf("Event dropped by one of the Client EventProcessors: %s\n", id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, processor := range globalEventProcessors {
|
||||
id := event.EventID
|
||||
event = processor(event, hint)
|
||||
if event == nil {
|
||||
Logger.Printf("Event dropped by one of the Global EventProcessors: %s\n", id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func (client Client) listIntegrations() []string {
|
||||
integrations := make([]string, 0, len(client.integrations))
|
||||
for _, integration := range client.integrations {
|
||||
integrations = append(integrations, integration.Name())
|
||||
}
|
||||
sort.Strings(integrations)
|
||||
return integrations
|
||||
}
|
||||
|
||||
func (client Client) integrationAlreadyInstalled(name string) bool {
|
||||
for _, integration := range client.integrations {
|
||||
if integration.Name() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
185
vendor/github.com/getsentry/sentry-go/dsn.go
generated
vendored
Normal file
185
vendor/github.com/getsentry/sentry-go/dsn.go
generated
vendored
Normal file
@ -0,0 +1,185 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type scheme string
|
||||
|
||||
const (
|
||||
schemeHTTP scheme = "http"
|
||||
schemeHTTPS scheme = "https"
|
||||
)
|
||||
|
||||
func (scheme scheme) defaultPort() int {
|
||||
switch scheme {
|
||||
case schemeHTTPS:
|
||||
return 443
|
||||
case schemeHTTP:
|
||||
return 80
|
||||
default:
|
||||
return 80
|
||||
}
|
||||
}
|
||||
|
||||
type DsnParseError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e DsnParseError) Error() string {
|
||||
return "[Sentry] DsnParseError: " + e.Message
|
||||
}
|
||||
|
||||
// Dsn is used as the remote address source to client transport.
|
||||
type Dsn struct {
|
||||
scheme scheme
|
||||
publicKey string
|
||||
secretKey string
|
||||
host string
|
||||
port int
|
||||
path string
|
||||
projectID int
|
||||
}
|
||||
|
||||
// NewDsn creates an instance od `Dsn` by parsing provided url in a `string` format.
|
||||
// If Dsn is not set the client is effectively disabled.
|
||||
func NewDsn(rawURL string) (*Dsn, error) {
|
||||
// Parse
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, &DsnParseError{"invalid url"}
|
||||
}
|
||||
|
||||
// Scheme
|
||||
var scheme scheme
|
||||
switch parsedURL.Scheme {
|
||||
case "http":
|
||||
scheme = schemeHTTP
|
||||
case "https":
|
||||
scheme = schemeHTTPS
|
||||
default:
|
||||
return nil, &DsnParseError{"invalid scheme"}
|
||||
}
|
||||
|
||||
// PublicKey
|
||||
publicKey := parsedURL.User.Username()
|
||||
if publicKey == "" {
|
||||
return nil, &DsnParseError{"empty username"}
|
||||
}
|
||||
|
||||
// SecretKey
|
||||
var secretKey string
|
||||
if parsedSecretKey, ok := parsedURL.User.Password(); ok {
|
||||
secretKey = parsedSecretKey
|
||||
}
|
||||
|
||||
// Host
|
||||
host := parsedURL.Hostname()
|
||||
if host == "" {
|
||||
return nil, &DsnParseError{"empty host"}
|
||||
}
|
||||
|
||||
// Port
|
||||
var port int
|
||||
if parsedURL.Port() != "" {
|
||||
parsedPort, err := strconv.Atoi(parsedURL.Port())
|
||||
if err != nil {
|
||||
return nil, &DsnParseError{"invalid port"}
|
||||
}
|
||||
port = parsedPort
|
||||
} else {
|
||||
port = scheme.defaultPort()
|
||||
}
|
||||
|
||||
// ProjectID
|
||||
if len(parsedURL.Path) == 0 || parsedURL.Path == "/" {
|
||||
return nil, &DsnParseError{"empty project id"}
|
||||
}
|
||||
pathSegments := strings.Split(parsedURL.Path[1:], "/")
|
||||
projectID, err := strconv.Atoi(pathSegments[len(pathSegments)-1])
|
||||
if err != nil {
|
||||
return nil, &DsnParseError{"invalid project id"}
|
||||
}
|
||||
|
||||
// Path
|
||||
var path string
|
||||
if len(pathSegments) > 1 {
|
||||
path = "/" + strings.Join(pathSegments[0:len(pathSegments)-1], "/")
|
||||
}
|
||||
|
||||
return &Dsn{
|
||||
scheme: scheme,
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey,
|
||||
host: host,
|
||||
port: port,
|
||||
path: path,
|
||||
projectID: projectID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String formats Dsn struct into a valid string url
|
||||
func (dsn Dsn) String() string {
|
||||
var url string
|
||||
url += fmt.Sprintf("%s://%s", dsn.scheme, dsn.publicKey)
|
||||
if dsn.secretKey != "" {
|
||||
url += fmt.Sprintf(":%s", dsn.secretKey)
|
||||
}
|
||||
url += fmt.Sprintf("@%s", dsn.host)
|
||||
if dsn.port != dsn.scheme.defaultPort() {
|
||||
url += fmt.Sprintf(":%d", dsn.port)
|
||||
}
|
||||
if dsn.path != "" {
|
||||
url += dsn.path
|
||||
}
|
||||
url += fmt.Sprintf("/%d", dsn.projectID)
|
||||
return url
|
||||
}
|
||||
|
||||
// StoreAPIURL returns assembled url to be used in the transport.
|
||||
// It points to configures Sentry instance.
|
||||
func (dsn Dsn) StoreAPIURL() *url.URL {
|
||||
var rawURL string
|
||||
rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host)
|
||||
if dsn.port != dsn.scheme.defaultPort() {
|
||||
rawURL += fmt.Sprintf(":%d", dsn.port)
|
||||
}
|
||||
rawURL += fmt.Sprintf("/api/%d/store/", dsn.projectID)
|
||||
parsedURL, _ := url.Parse(rawURL)
|
||||
return parsedURL
|
||||
}
|
||||
|
||||
// RequestHeaders returns all the necessary headers that have to be used in the transport.
|
||||
func (dsn Dsn) RequestHeaders() map[string]string {
|
||||
auth := fmt.Sprintf("Sentry sentry_version=%d, sentry_timestamp=%d, "+
|
||||
"sentry_client=sentry.go/%s, sentry_key=%s", 7, time.Now().Unix(), Version, dsn.publicKey)
|
||||
|
||||
if dsn.secretKey != "" {
|
||||
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"X-Sentry-Auth": auth,
|
||||
}
|
||||
}
|
||||
|
||||
func (dsn Dsn) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(dsn.String())
|
||||
}
|
||||
|
||||
func (dsn *Dsn) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
_ = json.Unmarshal(data, &str)
|
||||
newDsn, err := NewDsn(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*dsn = *newDsn
|
||||
return nil
|
||||
}
|
||||
7
vendor/github.com/getsentry/sentry-go/go.mod
generated
vendored
Normal file
7
vendor/github.com/getsentry/sentry-go/go.mod
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
module github.com/getsentry/sentry-go
|
||||
|
||||
require (
|
||||
github.com/go-errors/errors v1.0.1
|
||||
github.com/pingcap/errors v0.11.1
|
||||
github.com/pkg/errors v0.8.1
|
||||
)
|
||||
6
vendor/github.com/getsentry/sentry-go/go.sum
generated
vendored
Normal file
6
vendor/github.com/getsentry/sentry-go/go.sum
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/pingcap/errors v0.11.1 h1:BXFZ6MdDd2U1uJUa2sRAWTmm+nieEzuyYM0R4aUTcC8=
|
||||
github.com/pingcap/errors v0.11.1/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
135
vendor/github.com/getsentry/sentry-go/http/README.md
generated
vendored
Normal file
135
vendor/github.com/getsentry/sentry-go/http/README.md
generated
vendored
Normal file
@ -0,0 +1,135 @@
|
||||
<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 net/http Handler for Sentry-go SDK
|
||||
|
||||
**Godoc:** https://godoc.org/github.com/getsentry/sentry-go/http
|
||||
|
||||
**Example:** https://github.com/getsentry/sentry-go/tree/master/example/http
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
go get github.com/getsentry/sentry-go/http
|
||||
```
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
)
|
||||
|
||||
// To initialize Sentry's handler, you need to initialize Sentry itself beforehand
|
||||
if err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: "your-public-dsn",
|
||||
}); err != nil {
|
||||
fmt.Printf("Sentry initialization failed: %v\n", err)
|
||||
}
|
||||
|
||||
// Create an instance of sentryhttp
|
||||
sentryHandler := sentryhttp.New(sentryhttp.Options{})
|
||||
|
||||
// Once it's done, you can setup routes and attach the handler as one of your middleware
|
||||
http.Handle("/", sentryHandler.Handle(&handler{}))
|
||||
http.HandleFunc("/foo", sentryHandler.HandleFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
panic("y tho")
|
||||
}))
|
||||
|
||||
fmt.Println("Listening and serving HTTP on :3000")
|
||||
|
||||
// And run it
|
||||
if err := http.ListenAndServe(":3000", nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
`sentryhttp` accepts a struct of `Options` that allows you to configure how the handler will behave.
|
||||
|
||||
Currently it respects 3 options:
|
||||
|
||||
```go
|
||||
// Whether Sentry should repanic after recovery, in most cases it should be set to true,
|
||||
// and you should gracefully handle http responses.
|
||||
Repanic bool
|
||||
// Whether you want to block the request before moving forward with the response.
|
||||
// Useful, when you want to restart the process after it panics.
|
||||
WaitForDelivery bool
|
||||
// Timeout for the event delivery requests.
|
||||
Timeout time.Duration
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
`sentryhttp` attaches an instance of `*sentry.Hub` (https://godoc.org/github.com/getsentry/sentry-go#Hub) to the request's context, which makes it available throughout the rest of the request's lifetime.
|
||||
You can access it by using the `sentry.GetHubFromContext()` method on the request 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 `sentryhttp`!**
|
||||
|
||||
```go
|
||||
type handler struct{}
|
||||
|
||||
func (h *handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
if hub := sentry.GetHubFromContext(r.Context()); hub != nil {
|
||||
hub.WithScope(func(scope *sentry.Scope) {
|
||||
scope.SetExtra("unwantedQuery", "someQueryDataMaybe")
|
||||
hub.CaptureMessage("User provided unwanted query string, but we recovered just fine")
|
||||
})
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func enhanceSentryEvent(handler http.HandlerFunc) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
if hub := sentry.GetHubFromContext(r.Context()); hub != nil {
|
||||
hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt")
|
||||
}
|
||||
handler(rw, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Later in the code
|
||||
|
||||
sentryHandler := sentryhttp.New(sentryhttp.Options{
|
||||
Repanic: true,
|
||||
})
|
||||
|
||||
http.Handle("/", sentryHandler.Handle(&handler{}))
|
||||
http.HandleFunc("/foo", sentryHandler.HandleFunc(
|
||||
enhanceSentryEvent(func(rw http.ResponseWriter, r *http.Request) {
|
||||
panic("y tho")
|
||||
}),
|
||||
))
|
||||
|
||||
fmt.Println("Listening and serving HTTP on :3000")
|
||||
|
||||
if err := http.ListenAndServe(":3000", nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
},
|
||||
})
|
||||
```
|
||||
90
vendor/github.com/getsentry/sentry-go/http/sentryhttp.go
generated
vendored
Normal file
90
vendor/github.com/getsentry/sentry-go/http/sentryhttp.go
generated
vendored
Normal file
@ -0,0 +1,90 @@
|
||||
package sentryhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
repanic bool
|
||||
waitForDelivery bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
// Repanic configures whether Sentry should repanic after recovery, in most cases it should be set to true,
|
||||
// as iris.Default includes it's own Recovery middleware what handles http responses.
|
||||
Repanic bool
|
||||
// WaitForDelivery configures whether you want to block the request before moving forward with the response.
|
||||
// Because Iris's default `Recovery` handler doesn't restart the application,
|
||||
// it's safe to either skip this option or set it to `false`.
|
||||
WaitForDelivery bool
|
||||
// Timeout for the event delivery requests.
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// New returns a struct that provides Handle and HandleFunc methods
|
||||
// that satisfy http.Handler and http.HandlerFunc interfaces.
|
||||
func New(options Options) *Handler {
|
||||
handler := Handler{
|
||||
repanic: false,
|
||||
timeout: time.Second * 2,
|
||||
waitForDelivery: false,
|
||||
}
|
||||
|
||||
if options.Repanic {
|
||||
handler.repanic = true
|
||||
}
|
||||
|
||||
if options.WaitForDelivery {
|
||||
handler.waitForDelivery = true
|
||||
}
|
||||
|
||||
return &handler
|
||||
}
|
||||
|
||||
// Handle wraps http.Handler and recovers from caught panics.
|
||||
func (h *Handler) Handle(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
hub := sentry.CurrentHub().Clone()
|
||||
hub.Scope().SetRequest(sentry.Request{}.FromHTTPRequest(r))
|
||||
ctx := sentry.SetHubOnContext(
|
||||
r.Context(),
|
||||
hub,
|
||||
)
|
||||
defer h.recoverWithSentry(hub, r)
|
||||
handler.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// HandleFunc wraps http.HandleFunc and recovers from caught panics.
|
||||
func (h *Handler) HandleFunc(handler http.HandlerFunc) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
hub := sentry.CurrentHub().Clone()
|
||||
hub.Scope().SetRequest(sentry.Request{}.FromHTTPRequest(r))
|
||||
ctx := sentry.SetHubOnContext(
|
||||
r.Context(),
|
||||
hub,
|
||||
)
|
||||
defer h.recoverWithSentry(hub, r)
|
||||
handler(rw, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
277
vendor/github.com/getsentry/sentry-go/hub.go
generated
vendored
Normal file
277
vendor/github.com/getsentry/sentry-go/hub.go
generated
vendored
Normal file
@ -0,0 +1,277 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
// HubContextKey is a context key used to store Hub on any context.Context type
|
||||
const HubContextKey = contextKey(1)
|
||||
|
||||
// RequestContextKey is a context key used to store http.Request on the context passed to RecoverWithContext
|
||||
const RequestContextKey = contextKey(2)
|
||||
|
||||
// Default maximum number of breadcrumbs added to an event. Can be overwritten `maxBreadcrumbs` option.
|
||||
const defaultMaxBreadcrumbs = 30
|
||||
|
||||
// Absolute maximum number of breadcrumbs added to an event.
|
||||
// The `maxBreadcrumbs` option cannot be higher than this value.
|
||||
const maxBreadcrumbs = 100
|
||||
|
||||
// Initial instance of the Hub that has no `Client` bound and an empty `Scope`
|
||||
var currentHub = NewHub(nil, NewScope()) // nolint: gochecknoglobals
|
||||
|
||||
// Hub is the central object that can manages scopes and clients.
|
||||
//
|
||||
// This can be used to capture events and manage the scope.
|
||||
// The default hub that is available automatically.
|
||||
//
|
||||
// In most situations developers do not need to interface the hub. Instead
|
||||
// toplevel convenience functions are exposed that will automatically dispatch
|
||||
// to global (`CurrentHub`) hub. In some situations this might not be
|
||||
// possible in which case it might become necessary to manually work with the
|
||||
// hub. This is for instance the case when working with async code.
|
||||
type Hub struct {
|
||||
stack *stack
|
||||
lastEventID EventID
|
||||
}
|
||||
|
||||
type layer struct {
|
||||
client *Client
|
||||
scope *Scope
|
||||
}
|
||||
|
||||
type stack []*layer
|
||||
|
||||
// NewHub returns an instance of a `Hub` with provided `Client` and `Scope` bound.
|
||||
func NewHub(client *Client, scope *Scope) *Hub {
|
||||
hub := Hub{
|
||||
stack: &stack{{
|
||||
client: client,
|
||||
scope: scope,
|
||||
}},
|
||||
}
|
||||
return &hub
|
||||
}
|
||||
|
||||
// CurrentHub returns an instance of previously initialized `Hub` stored in the global namespace.
|
||||
func CurrentHub() *Hub {
|
||||
return currentHub
|
||||
}
|
||||
|
||||
// LastEventID returns an ID of last captured event for the current `Hub`.
|
||||
func (hub *Hub) LastEventID() EventID {
|
||||
return hub.lastEventID
|
||||
}
|
||||
|
||||
func (hub *Hub) stackTop() *layer {
|
||||
stack := hub.stack
|
||||
if stack == nil || len(*stack) == 0 {
|
||||
return nil
|
||||
}
|
||||
return (*stack)[len(*stack)-1]
|
||||
}
|
||||
|
||||
// Clone returns a copy of the current Hub with top-most scope and client copied over.
|
||||
func (hub *Hub) Clone() *Hub {
|
||||
return NewHub(hub.Client(), hub.Scope().Clone())
|
||||
}
|
||||
|
||||
// Scope returns top-level `Scope` of the current `Hub` or `nil` if no `Scope` is bound.
|
||||
func (hub *Hub) Scope() *Scope {
|
||||
top := hub.stackTop()
|
||||
if top == nil {
|
||||
return nil
|
||||
}
|
||||
return top.scope
|
||||
}
|
||||
|
||||
// Scope returns top-level `Client` of the current `Hub` or `nil` if no `Client` is bound.
|
||||
func (hub *Hub) Client() *Client {
|
||||
top := hub.stackTop()
|
||||
if top == nil {
|
||||
return nil
|
||||
}
|
||||
return top.client
|
||||
}
|
||||
|
||||
// PushScope pushes a new scope for the current `Hub` and reuses previously bound `Client`.
|
||||
func (hub *Hub) PushScope() *Scope {
|
||||
scope := hub.Scope().Clone()
|
||||
|
||||
*hub.stack = append(*hub.stack, &layer{
|
||||
client: hub.Client(),
|
||||
scope: scope,
|
||||
})
|
||||
|
||||
return scope
|
||||
}
|
||||
|
||||
// PushScope pops the most recent scope for the current `Hub`.
|
||||
func (hub *Hub) PopScope() {
|
||||
stack := *hub.stack
|
||||
if len(stack) == 0 {
|
||||
return
|
||||
}
|
||||
*hub.stack = stack[0 : len(stack)-1]
|
||||
}
|
||||
|
||||
// BindClient binds a new `Client` for the current `Hub`.
|
||||
func (hub *Hub) BindClient(client *Client) {
|
||||
hub.stackTop().client = client
|
||||
}
|
||||
|
||||
// WithScope temporarily pushes a scope for a single call.
|
||||
//
|
||||
// A shorthand for:
|
||||
// PushScope()
|
||||
// f(scope)
|
||||
// PopScope()
|
||||
func (hub *Hub) WithScope(f func(scope *Scope)) {
|
||||
scope := hub.PushScope()
|
||||
defer hub.PopScope()
|
||||
f(scope)
|
||||
}
|
||||
|
||||
// ConfigureScope invokes a function that can modify the current scope.
|
||||
//
|
||||
// The function is passed a mutable reference to the `Scope` so that modifications
|
||||
// can be performed.
|
||||
func (hub *Hub) ConfigureScope(f func(scope *Scope)) {
|
||||
f(hub.Scope())
|
||||
}
|
||||
|
||||
// CaptureEvent calls the method of a same name on currently bound `Client` instance
|
||||
// passing it a top-level `Scope`.
|
||||
// Returns `EventID` if successfully, or `nil` if there's no `Scope` or `Client` available.
|
||||
func (hub *Hub) CaptureEvent(event *Event) *EventID {
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil || scope == nil {
|
||||
return nil
|
||||
}
|
||||
return client.CaptureEvent(event, nil, scope)
|
||||
}
|
||||
|
||||
// CaptureMessage calls the method of a same name on currently bound `Client` instance
|
||||
// passing it a top-level `Scope`.
|
||||
// Returns `EventID` if successfully, or `nil` if there's no `Scope` or `Client` available.
|
||||
func (hub *Hub) CaptureMessage(message string) *EventID {
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil || scope == nil {
|
||||
return nil
|
||||
}
|
||||
return client.CaptureMessage(message, nil, scope)
|
||||
}
|
||||
|
||||
// CaptureException calls the method of a same name on currently bound `Client` instance
|
||||
// passing it a top-level `Scope`.
|
||||
// Returns `EventID` if successfully, or `nil` if there's no `Scope` or `Client` available.
|
||||
func (hub *Hub) CaptureException(exception error) *EventID {
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil || scope == nil {
|
||||
return nil
|
||||
}
|
||||
return client.CaptureException(exception, &EventHint{OriginalException: exception}, scope)
|
||||
}
|
||||
|
||||
// AddBreadcrumb records a new breadcrumb.
|
||||
//
|
||||
// The total number of breadcrumbs that can be recorded are limited by the
|
||||
// configuration on the client.
|
||||
func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
|
||||
client := hub.Client()
|
||||
|
||||
// If there's no client, just store it on the scope straight away
|
||||
if client == nil {
|
||||
hub.Scope().AddBreadcrumb(breadcrumb, maxBreadcrumbs)
|
||||
return
|
||||
}
|
||||
|
||||
options := client.Options()
|
||||
max := defaultMaxBreadcrumbs
|
||||
|
||||
if options.MaxBreadcrumbs != 0 {
|
||||
max = options.MaxBreadcrumbs
|
||||
}
|
||||
|
||||
if max < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if options.BeforeBreadcrumb != nil {
|
||||
h := &BreadcrumbHint{}
|
||||
if hint != nil {
|
||||
h = hint
|
||||
}
|
||||
if breadcrumb = options.BeforeBreadcrumb(breadcrumb, h); breadcrumb == nil {
|
||||
Logger.Println("breadcrumb dropped due to BeforeBreadcrumb callback")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if max > maxBreadcrumbs {
|
||||
max = maxBreadcrumbs
|
||||
}
|
||||
hub.Scope().AddBreadcrumb(breadcrumb, max)
|
||||
}
|
||||
|
||||
// Recover calls the method of a same name on currently bound `Client` instance
|
||||
// passing it a top-level `Scope`.
|
||||
// Returns `EventID` if successfully, or `nil` if there's no `Scope` or `Client` available.
|
||||
func (hub *Hub) Recover(err interface{}) *EventID {
|
||||
if err == nil {
|
||||
err = recover()
|
||||
}
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil || scope == nil {
|
||||
return nil
|
||||
}
|
||||
return client.Recover(err, &EventHint{RecoveredException: err}, scope)
|
||||
}
|
||||
|
||||
// RecoverWithContext calls the method of a same name on currently bound `Client` instance
|
||||
// passing it a top-level `Scope`.
|
||||
// Returns `EventID` if successfully, or `nil` if there's no `Scope` or `Client` available.
|
||||
func (hub *Hub) RecoverWithContext(ctx context.Context, err interface{}) *EventID {
|
||||
if err == nil {
|
||||
err = recover()
|
||||
}
|
||||
client, scope := hub.Client(), hub.Scope()
|
||||
if client == nil || scope == nil {
|
||||
return nil
|
||||
}
|
||||
return client.RecoverWithContext(ctx, err, &EventHint{RecoveredException: err}, scope)
|
||||
}
|
||||
|
||||
// Flush calls the method of a same name on currently bound `Client` instance.
|
||||
func (hub *Hub) Flush(timeout time.Duration) bool {
|
||||
client := hub.Client()
|
||||
|
||||
if client == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return client.Flush(timeout)
|
||||
}
|
||||
|
||||
// HasHubOnContext checks whether `Hub` instance is bound to a given `Context` struct.
|
||||
func HasHubOnContext(ctx context.Context) bool {
|
||||
_, ok := ctx.Value(HubContextKey).(*Hub)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetHubFromContext tries to retrieve `Hub` instance from the given `Context` struct
|
||||
// or return `nil` if one is not found.
|
||||
func GetHubFromContext(ctx context.Context) *Hub {
|
||||
if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
|
||||
return hub
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetHubOnContext stores given `Hub` instance on the `Context` struct and returns a new `Context`.
|
||||
func SetHubOnContext(ctx context.Context, hub *Hub) context.Context {
|
||||
return context.WithValue(ctx, HubContextKey, hub)
|
||||
}
|
||||
262
vendor/github.com/getsentry/sentry-go/integrations.go
generated
vendored
Normal file
262
vendor/github.com/getsentry/sentry-go/integrations.go
generated
vendored
Normal file
@ -0,0 +1,262 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ================================
|
||||
// Modules Integration
|
||||
// ================================
|
||||
|
||||
type modulesIntegration struct{}
|
||||
|
||||
var _modulesCache map[string]string // nolint: gochecknoglobals
|
||||
|
||||
func (mi *modulesIntegration) Name() string {
|
||||
return "Modules"
|
||||
}
|
||||
|
||||
func (mi *modulesIntegration) SetupOnce(client *Client) {
|
||||
client.AddEventProcessor(mi.processor)
|
||||
}
|
||||
|
||||
func (mi *modulesIntegration) processor(event *Event, hint *EventHint) *Event {
|
||||
if event.Modules == nil {
|
||||
event.Modules = extractModules()
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func extractModules() map[string]string {
|
||||
if _modulesCache != nil {
|
||||
return _modulesCache
|
||||
}
|
||||
|
||||
extractedModules, err := getModules()
|
||||
if err != nil {
|
||||
Logger.Printf("ModuleIntegration wasn't able to extract modules: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
_modulesCache = extractedModules
|
||||
|
||||
return extractedModules
|
||||
}
|
||||
|
||||
func getModules() (map[string]string, error) {
|
||||
if fileExists("go.mod") {
|
||||
return getModulesFromMod()
|
||||
}
|
||||
|
||||
if fileExists("vendor") {
|
||||
// Priority given to vendor created by modules
|
||||
if fileExists("vendor/modules.txt") {
|
||||
return getModulesFromVendorTxt()
|
||||
}
|
||||
|
||||
if fileExists("vendor/vendor.json") {
|
||||
return getModulesFromVendorJSON()
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("module integration failed")
|
||||
}
|
||||
|
||||
func getModulesFromMod() (map[string]string, error) {
|
||||
modules := make(map[string]string)
|
||||
|
||||
file, err := os.Open("go.mod")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open mod file")
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
areModulesPresent := false
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
splits := strings.Split(scanner.Text(), " ")
|
||||
|
||||
if splits[0] == "require" {
|
||||
areModulesPresent = true
|
||||
|
||||
// Mod file has only 1 dependency
|
||||
if len(splits) > 2 {
|
||||
modules[strings.TrimSpace(splits[1])] = splits[2]
|
||||
return modules, nil
|
||||
}
|
||||
} else if areModulesPresent && splits[0] != ")" {
|
||||
modules[strings.TrimSpace(splits[0])] = splits[1]
|
||||
}
|
||||
}
|
||||
|
||||
if scannerErr := scanner.Err(); scannerErr != nil {
|
||||
return nil, scannerErr
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
func getModulesFromVendorTxt() (map[string]string, error) {
|
||||
modules := make(map[string]string)
|
||||
|
||||
file, err := os.Open("vendor/modules.txt")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open vendor/modules.txt")
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
splits := strings.Split(scanner.Text(), " ")
|
||||
|
||||
if splits[0] == "#" {
|
||||
modules[splits[1]] = splits[2]
|
||||
}
|
||||
}
|
||||
|
||||
if scannerErr := scanner.Err(); scannerErr != nil {
|
||||
return nil, scannerErr
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
func getModulesFromVendorJSON() (map[string]string, error) {
|
||||
modules := make(map[string]string)
|
||||
|
||||
file, err := ioutil.ReadFile("vendor/vendor.json")
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open vendor/vendor.json")
|
||||
}
|
||||
|
||||
var vendor map[string]interface{}
|
||||
if unmarshalErr := json.Unmarshal(file, &vendor); unmarshalErr != nil {
|
||||
return nil, unmarshalErr
|
||||
}
|
||||
|
||||
packages := vendor["package"].([]interface{})
|
||||
|
||||
// To avoid iterative dependencies, TODO: Change of default value
|
||||
lastPath := "\n"
|
||||
|
||||
for _, value := range packages {
|
||||
path := value.(map[string]interface{})["path"].(string)
|
||||
|
||||
if !strings.Contains(path, lastPath) {
|
||||
// No versions are available through vendor.json
|
||||
modules[path] = ""
|
||||
lastPath = path
|
||||
}
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Environment Integration
|
||||
// ================================
|
||||
|
||||
type environmentIntegration struct{}
|
||||
|
||||
func (ei *environmentIntegration) Name() string {
|
||||
return "Environment"
|
||||
}
|
||||
|
||||
func (ei *environmentIntegration) SetupOnce(client *Client) {
|
||||
client.AddEventProcessor(ei.processor)
|
||||
}
|
||||
|
||||
func (ei *environmentIntegration) processor(event *Event, hint *EventHint) *Event {
|
||||
if event.Contexts == nil {
|
||||
event.Contexts = make(map[string]interface{})
|
||||
}
|
||||
|
||||
event.Contexts["device"] = map[string]interface{}{
|
||||
"arch": runtime.GOARCH,
|
||||
"num_cpu": runtime.NumCPU(),
|
||||
}
|
||||
|
||||
event.Contexts["os"] = map[string]interface{}{
|
||||
"name": runtime.GOOS,
|
||||
}
|
||||
|
||||
event.Contexts["runtime"] = map[string]interface{}{
|
||||
"name": "go",
|
||||
"version": runtime.Version(),
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Ignore Errors Integration
|
||||
// ================================
|
||||
|
||||
type ignoreErrorsIntegration struct {
|
||||
ignoreErrors []*regexp.Regexp
|
||||
}
|
||||
|
||||
func (iei *ignoreErrorsIntegration) Name() string {
|
||||
return "IgnoreErrors"
|
||||
}
|
||||
|
||||
func (iei *ignoreErrorsIntegration) SetupOnce(client *Client) {
|
||||
iei.ignoreErrors = transformStringsIntoRegexps(client.Options().IgnoreErrors)
|
||||
client.AddEventProcessor(iei.processor)
|
||||
}
|
||||
|
||||
func (iei *ignoreErrorsIntegration) processor(event *Event, hint *EventHint) *Event {
|
||||
suspects := getIgnoreErrorsSuspects(event)
|
||||
|
||||
for _, suspect := range suspects {
|
||||
for _, pattern := range iei.ignoreErrors {
|
||||
if pattern.Match([]byte(suspect)) {
|
||||
Logger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
|
||||
"| Value matched: %s | Filter used: %s", suspect, pattern)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
func transformStringsIntoRegexps(strings []string) []*regexp.Regexp {
|
||||
var exprs []*regexp.Regexp
|
||||
|
||||
for _, s := range strings {
|
||||
r, err := regexp.Compile(s)
|
||||
if err == nil {
|
||||
exprs = append(exprs, r)
|
||||
}
|
||||
}
|
||||
|
||||
return exprs
|
||||
}
|
||||
|
||||
func getIgnoreErrorsSuspects(event *Event) []string {
|
||||
suspects := []string{}
|
||||
|
||||
if event.Message != "" {
|
||||
suspects = append(suspects, event.Message)
|
||||
}
|
||||
|
||||
for _, ex := range event.Exception {
|
||||
suspects = append(suspects, ex.Type)
|
||||
suspects = append(suspects, ex.Value)
|
||||
}
|
||||
|
||||
return suspects
|
||||
}
|
||||
180
vendor/github.com/getsentry/sentry-go/interfaces.go
generated
vendored
Normal file
180
vendor/github.com/getsentry/sentry-go/interfaces.go
generated
vendored
Normal file
@ -0,0 +1,180 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Protocol Docs (kinda)
|
||||
// https://github.com/getsentry/rust-sentry-types/blob/master/src/protocol/v7.rs
|
||||
|
||||
// Level marks the severity of the event
|
||||
type Level string
|
||||
|
||||
const (
|
||||
LevelDebug Level = "debug"
|
||||
LevelInfo Level = "info"
|
||||
LevelWarning Level = "warning"
|
||||
LevelError Level = "error"
|
||||
LevelFatal Level = "fatal"
|
||||
)
|
||||
|
||||
// https://docs.sentry.io/development/sdk-dev/interfaces/sdk/
|
||||
type SdkInfo struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Integrations []string `json:"integrations,omitempty"`
|
||||
Packages []SdkPackage `json:"packages,omitempty"`
|
||||
}
|
||||
|
||||
type SdkPackage struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// TODO: This type could be more useful, as map of interface{} is too generic
|
||||
// and requires a lot of type assertions in beforeBreadcrumb calls
|
||||
// plus it could just be `map[string]interface{}` then
|
||||
type BreadcrumbHint map[string]interface{}
|
||||
|
||||
// https://docs.sentry.io/development/sdk-dev/interfaces/breadcrumbs/
|
||||
type Breadcrumb struct {
|
||||
Category string `json:"category,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
Level Level `json:"level,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// https://docs.sentry.io/development/sdk-dev/interfaces/user/
|
||||
type User struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
IPAddress string `json:"ip_address,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
}
|
||||
|
||||
// https://docs.sentry.io/development/sdk-dev/interfaces/http/
|
||||
type Request struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
QueryString string `json:"query_string,omitempty"`
|
||||
Cookies string `json:"cookies,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
func (r Request) FromHTTPRequest(request *http.Request) Request {
|
||||
// Method
|
||||
r.Method = request.Method
|
||||
|
||||
// URL
|
||||
protocol := schemeHTTP
|
||||
if request.TLS != nil || request.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
protocol = schemeHTTPS
|
||||
}
|
||||
r.URL = fmt.Sprintf("%s://%s%s", protocol, request.Host, request.URL.Path)
|
||||
|
||||
// Headers
|
||||
headers := make(map[string]string, len(request.Header))
|
||||
for k, v := range request.Header {
|
||||
headers[k] = strings.Join(v, ",")
|
||||
}
|
||||
headers["Host"] = request.Host
|
||||
r.Headers = headers
|
||||
|
||||
// Cookies
|
||||
r.Cookies = request.Header.Get("Cookie")
|
||||
|
||||
// Env
|
||||
if addr, port, err := net.SplitHostPort(request.RemoteAddr); err == nil {
|
||||
r.Env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
|
||||
}
|
||||
|
||||
// QueryString
|
||||
r.QueryString = request.URL.RawQuery
|
||||
|
||||
// Body
|
||||
if request.GetBody != nil {
|
||||
if bodyCopy, err := request.GetBody(); err == nil && bodyCopy != nil {
|
||||
body, err := ioutil.ReadAll(bodyCopy)
|
||||
if err == nil {
|
||||
r.Data = string(body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// https://docs.sentry.io/development/sdk-dev/interfaces/exception/
|
||||
type Exception struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Module string `json:"module,omitempty"`
|
||||
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
||||
RawStacktrace *Stacktrace `json:"raw_stacktrace,omitempty"`
|
||||
}
|
||||
|
||||
type EventID string
|
||||
|
||||
// https://docs.sentry.io/development/sdk-dev/attributes/
|
||||
type Event struct {
|
||||
Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"`
|
||||
Contexts map[string]interface{} `json:"contexts,omitempty"`
|
||||
Dist string `json:"dist,omitempty"`
|
||||
Environment string `json:"environment,omitempty"`
|
||||
EventID EventID `json:"event_id,omitempty"`
|
||||
Extra map[string]interface{} `json:"extra,omitempty"`
|
||||
Fingerprint []string `json:"fingerprint,omitempty"`
|
||||
Level Level `json:"level,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Release string `json:"release,omitempty"`
|
||||
Sdk SdkInfo `json:"sdk,omitempty"`
|
||||
ServerName string `json:"server_name,omitempty"`
|
||||
Threads []Thread `json:"threads,omitempty"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
Transaction string `json:"transaction,omitempty"`
|
||||
User User `json:"user,omitempty"`
|
||||
Logger string `json:"logger,omitempty"`
|
||||
Modules map[string]string `json:"modules,omitempty"`
|
||||
Request Request `json:"request,omitempty"`
|
||||
Exception []Exception `json:"exception,omitempty"`
|
||||
}
|
||||
|
||||
func NewEvent() *Event {
|
||||
event := Event{
|
||||
Contexts: make(map[string]interface{}),
|
||||
Extra: make(map[string]interface{}),
|
||||
Tags: make(map[string]string),
|
||||
Modules: make(map[string]string),
|
||||
}
|
||||
return &event
|
||||
}
|
||||
|
||||
type Thread struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
||||
RawStacktrace *Stacktrace `json:"raw_stacktrace,omitempty"`
|
||||
Crashed bool `json:"crashed,omitempty"`
|
||||
Current bool `json:"current,omitempty"`
|
||||
}
|
||||
|
||||
type EventHint struct {
|
||||
Data interface{}
|
||||
EventID string
|
||||
OriginalException error
|
||||
RecoveredException interface{}
|
||||
Context context.Context
|
||||
Request *http.Request
|
||||
Response *http.Response
|
||||
}
|
||||
238
vendor/github.com/getsentry/sentry-go/scope.go
generated
vendored
Normal file
238
vendor/github.com/getsentry/sentry-go/scope.go
generated
vendored
Normal file
@ -0,0 +1,238 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Scope holds contextual data for the current scope.
|
||||
//
|
||||
// The scope is an object that can cloned efficiently and stores data that
|
||||
// is locally relevant to an event. For instance the scope will hold recorded
|
||||
// breadcrumbs and similar information.
|
||||
//
|
||||
// The scope can be interacted with in two ways:
|
||||
//
|
||||
// 1. the scope is routinely updated with information by functions such as
|
||||
// `AddBreadcrumb` which will modify the currently top-most scope.
|
||||
// 2. the topmost scope can also be configured through the `ConfigureScope`
|
||||
// method.
|
||||
//
|
||||
// Note that the scope can only be modified but not inspected.
|
||||
// Only the client can use the scope to extract information currently.
|
||||
type Scope struct {
|
||||
breadcrumbs []*Breadcrumb
|
||||
user User
|
||||
tags map[string]string
|
||||
contexts map[string]interface{}
|
||||
extra map[string]interface{}
|
||||
fingerprint []string
|
||||
level Level
|
||||
request Request
|
||||
eventProcessors []EventProcessor
|
||||
}
|
||||
|
||||
func NewScope() *Scope {
|
||||
scope := Scope{
|
||||
breadcrumbs: make([]*Breadcrumb, 0),
|
||||
tags: make(map[string]string),
|
||||
contexts: make(map[string]interface{}),
|
||||
extra: make(map[string]interface{}),
|
||||
fingerprint: make([]string, 0),
|
||||
}
|
||||
|
||||
return &scope
|
||||
}
|
||||
|
||||
// AddBreadcrumb adds new breadcrumb to the current scope
|
||||
// and optionaly throws the old one if limit is reached.
|
||||
func (scope *Scope) AddBreadcrumb(breadcrumb *Breadcrumb, limit int) {
|
||||
if breadcrumb.Timestamp == 0 {
|
||||
breadcrumb.Timestamp = time.Now().Unix()
|
||||
}
|
||||
|
||||
breadcrumbs := append(scope.breadcrumbs, breadcrumb)
|
||||
if len(breadcrumbs) > limit {
|
||||
scope.breadcrumbs = breadcrumbs[1 : limit+1]
|
||||
} else {
|
||||
scope.breadcrumbs = breadcrumbs
|
||||
}
|
||||
}
|
||||
|
||||
// ClearBreadcrumbs clears all breadcrumbs from the current scope.
|
||||
func (scope *Scope) ClearBreadcrumbs() {
|
||||
scope.breadcrumbs = []*Breadcrumb{}
|
||||
}
|
||||
|
||||
// SetUser sets new user for the current scope.
|
||||
func (scope *Scope) SetUser(user User) {
|
||||
scope.user = user
|
||||
}
|
||||
|
||||
// SetRequest sets new user for the current scope.
|
||||
func (scope *Scope) SetRequest(request Request) {
|
||||
scope.request = request
|
||||
}
|
||||
|
||||
// SetTag adds a tag to the current scope.
|
||||
func (scope *Scope) SetTag(key, value string) {
|
||||
scope.tags[key] = value
|
||||
}
|
||||
|
||||
// SetTags assigns multiple tags to the current scope.
|
||||
func (scope *Scope) SetTags(tags map[string]string) {
|
||||
for k, v := range tags {
|
||||
scope.tags[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveTag removes a tag from the current scope.
|
||||
func (scope *Scope) RemoveTag(key string) {
|
||||
delete(scope.tags, key)
|
||||
}
|
||||
|
||||
// SetContext adds a context to the current scope.
|
||||
func (scope *Scope) SetContext(key string, value interface{}) {
|
||||
scope.contexts[key] = value
|
||||
}
|
||||
|
||||
// SetContexts assigns multiple contexts to the current scope.
|
||||
func (scope *Scope) SetContexts(contexts map[string]interface{}) {
|
||||
for k, v := range contexts {
|
||||
scope.contexts[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveContext removes a context from the current scope.
|
||||
func (scope *Scope) RemoveContext(key string) {
|
||||
delete(scope.contexts, key)
|
||||
}
|
||||
|
||||
// SetExtra adds an extra to the current scope.
|
||||
func (scope *Scope) SetExtra(key string, value interface{}) {
|
||||
scope.extra[key] = value
|
||||
}
|
||||
|
||||
// SetExtras assigns multiple extras to the current scope.
|
||||
func (scope *Scope) SetExtras(extra map[string]interface{}) {
|
||||
for k, v := range extra {
|
||||
scope.extra[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveExtra removes a extra from the current scope.
|
||||
func (scope *Scope) RemoveExtra(key string) {
|
||||
delete(scope.extra, key)
|
||||
}
|
||||
|
||||
// SetFingerprint sets new fingerprint for the current scope.
|
||||
func (scope *Scope) SetFingerprint(fingerprint []string) {
|
||||
scope.fingerprint = fingerprint
|
||||
}
|
||||
|
||||
// SetLevel sets new level for the current scope.
|
||||
func (scope *Scope) SetLevel(level Level) {
|
||||
scope.level = level
|
||||
}
|
||||
|
||||
// Clone returns a copy of the current scope with all data copied over.
|
||||
func (scope *Scope) Clone() *Scope {
|
||||
clone := NewScope()
|
||||
clone.user = scope.user
|
||||
clone.breadcrumbs = make([]*Breadcrumb, len(scope.breadcrumbs))
|
||||
copy(clone.breadcrumbs, scope.breadcrumbs)
|
||||
for key, value := range scope.tags {
|
||||
clone.tags[key] = value
|
||||
}
|
||||
for key, value := range scope.contexts {
|
||||
clone.contexts[key] = value
|
||||
}
|
||||
for key, value := range scope.extra {
|
||||
clone.extra[key] = value
|
||||
}
|
||||
clone.fingerprint = make([]string, len(scope.fingerprint))
|
||||
copy(clone.fingerprint, scope.fingerprint)
|
||||
clone.level = scope.level
|
||||
clone.request = scope.request
|
||||
return clone
|
||||
}
|
||||
|
||||
// Clear removed the data from the current scope.
|
||||
func (scope *Scope) Clear() {
|
||||
*scope = Scope{}
|
||||
}
|
||||
|
||||
// AddEventProcessor adds an event processor to the current scope.
|
||||
func (scope *Scope) AddEventProcessor(processor EventProcessor) {
|
||||
scope.eventProcessors = append(scope.eventProcessors, processor)
|
||||
}
|
||||
|
||||
// ApplyToEvent takes the data from the current scope and attaches it to the event.
|
||||
func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
|
||||
if len(scope.breadcrumbs) > 0 {
|
||||
if event.Breadcrumbs == nil {
|
||||
event.Breadcrumbs = []*Breadcrumb{}
|
||||
}
|
||||
|
||||
event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...)
|
||||
}
|
||||
|
||||
if len(scope.tags) > 0 {
|
||||
if event.Tags == nil {
|
||||
event.Tags = make(map[string]string)
|
||||
}
|
||||
|
||||
for key, value := range scope.tags {
|
||||
event.Tags[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if len(scope.contexts) > 0 {
|
||||
if event.Contexts == nil {
|
||||
event.Contexts = make(map[string]interface{})
|
||||
}
|
||||
|
||||
for key, value := range scope.contexts {
|
||||
event.Contexts[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if len(scope.extra) > 0 {
|
||||
if event.Extra == nil {
|
||||
event.Extra = make(map[string]interface{})
|
||||
}
|
||||
|
||||
for key, value := range scope.extra {
|
||||
event.Extra[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (reflect.DeepEqual(event.User, User{})) {
|
||||
event.User = scope.user
|
||||
}
|
||||
|
||||
if (event.Fingerprint == nil || len(event.Fingerprint) == 0) &&
|
||||
len(scope.fingerprint) > 0 {
|
||||
event.Fingerprint = make([]string, len(scope.fingerprint))
|
||||
copy(event.Fingerprint, scope.fingerprint)
|
||||
}
|
||||
|
||||
if scope.level != "" {
|
||||
event.Level = scope.level
|
||||
}
|
||||
|
||||
if (reflect.DeepEqual(event.Request, Request{})) {
|
||||
event.Request = scope.request
|
||||
}
|
||||
|
||||
for _, processor := range scope.eventProcessors {
|
||||
id := event.EventID
|
||||
event = processor(event, hint)
|
||||
if event == nil {
|
||||
Logger.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
122
vendor/github.com/getsentry/sentry-go/sentry.go
generated
vendored
Normal file
122
vendor/github.com/getsentry/sentry-go/sentry.go
generated
vendored
Normal file
@ -0,0 +1,122 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Version Sentry-Go SDK Version
|
||||
const Version = "0.1.1"
|
||||
|
||||
// Init initializes whole SDK by creating new `Client` and binding it to the current `Hub`
|
||||
func Init(options ClientOptions) error {
|
||||
hub := CurrentHub()
|
||||
client, err := NewClient(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hub.BindClient(client)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddBreadcrumb records a new breadcrumb.
|
||||
//
|
||||
// The total number of breadcrumbs that can be recorded are limited by the
|
||||
// configuration on the client.
|
||||
func AddBreadcrumb(breadcrumb *Breadcrumb) {
|
||||
hub := CurrentHub()
|
||||
hub.AddBreadcrumb(breadcrumb, nil)
|
||||
}
|
||||
|
||||
// CaptureMessage captures an arbitrary message.
|
||||
func CaptureMessage(message string) *EventID {
|
||||
hub := CurrentHub()
|
||||
return hub.CaptureMessage(message)
|
||||
}
|
||||
|
||||
// CaptureException captures an error.
|
||||
func CaptureException(exception error) *EventID {
|
||||
hub := CurrentHub()
|
||||
return hub.CaptureException(exception)
|
||||
}
|
||||
|
||||
// CaptureEvent captures an event on the currently active client if any.
|
||||
//
|
||||
// The event must already be assembled. Typically code would instead use
|
||||
// the utility methods like `CaptureException`. The return value is the
|
||||
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
|
||||
func CaptureEvent(event *Event) *EventID {
|
||||
hub := CurrentHub()
|
||||
return hub.CaptureEvent(event)
|
||||
}
|
||||
|
||||
// Recover captures a panic.
|
||||
func Recover() *EventID {
|
||||
if err := recover(); err != nil {
|
||||
hub := CurrentHub()
|
||||
return hub.Recover(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recover captures a panic and passes relevant context object.
|
||||
func RecoverWithContext(ctx context.Context) *EventID {
|
||||
if err := recover(); err != nil {
|
||||
var hub *Hub
|
||||
|
||||
if HasHubOnContext(ctx) {
|
||||
hub = GetHubFromContext(ctx)
|
||||
} else {
|
||||
hub = CurrentHub()
|
||||
}
|
||||
|
||||
return hub.RecoverWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithScope temporarily pushes a scope for a single call.
|
||||
//
|
||||
// This function takes one argument, a callback that executes
|
||||
// in the context of that scope.
|
||||
//
|
||||
// This is useful when extra data should be send with a single capture call
|
||||
// for instance a different level or tags
|
||||
func WithScope(f func(scope *Scope)) {
|
||||
hub := CurrentHub()
|
||||
hub.WithScope(f)
|
||||
}
|
||||
|
||||
// ConfigureScope invokes a function that can modify the current scope.
|
||||
//
|
||||
// The function is passed a mutable reference to the `Scope` so that modifications
|
||||
// can be performed.
|
||||
func ConfigureScope(f func(scope *Scope)) {
|
||||
hub := CurrentHub()
|
||||
hub.ConfigureScope(f)
|
||||
}
|
||||
|
||||
// PushScope pushes a new scope.
|
||||
func PushScope() {
|
||||
hub := CurrentHub()
|
||||
hub.PushScope()
|
||||
}
|
||||
|
||||
// PopScope pushes a new scope.
|
||||
func PopScope() {
|
||||
hub := CurrentHub()
|
||||
hub.PopScope()
|
||||
}
|
||||
|
||||
// Flush notifies when all the buffered events have been sent by returning `true`
|
||||
// or `false` if timeout was reached.
|
||||
func Flush(timeout time.Duration) bool {
|
||||
hub := CurrentHub()
|
||||
return hub.Flush(timeout)
|
||||
}
|
||||
|
||||
// LastEventID returns an ID of last captured event.
|
||||
func LastEventID() EventID {
|
||||
hub := CurrentHub()
|
||||
return hub.LastEventID()
|
||||
}
|
||||
69
vendor/github.com/getsentry/sentry-go/sourcereader.go
generated
vendored
Normal file
69
vendor/github.com/getsentry/sentry-go/sourcereader.go
generated
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type sourceReader struct {
|
||||
mu sync.Mutex
|
||||
cache map[string][][]byte
|
||||
}
|
||||
|
||||
func newSourceReader() sourceReader {
|
||||
return sourceReader{
|
||||
cache: make(map[string][][]byte),
|
||||
}
|
||||
}
|
||||
|
||||
func (sr *sourceReader) readContextLines(filename string, line, context int) ([][]byte, int) {
|
||||
sr.mu.Lock()
|
||||
defer sr.mu.Unlock()
|
||||
|
||||
lines, ok := sr.cache[filename]
|
||||
|
||||
if !ok {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
sr.cache[filename] = nil
|
||||
return nil, 0
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
sr.cache[filename] = lines
|
||||
}
|
||||
|
||||
return sr.calculateContextLines(lines, line, context)
|
||||
}
|
||||
|
||||
// `initial` points to a line that's the `context_line` itself in relation to returned slice
|
||||
func (sr *sourceReader) calculateContextLines(lines [][]byte, line, context int) ([][]byte, int) {
|
||||
// Stacktrace lines are 1-indexed, slices are 0-indexed
|
||||
line--
|
||||
|
||||
initial := context
|
||||
|
||||
if lines == nil || line >= len(lines) || line < 0 {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
if context < 0 {
|
||||
context = 0
|
||||
initial = 0
|
||||
}
|
||||
|
||||
start := line - context
|
||||
|
||||
if start < 0 {
|
||||
initial += start
|
||||
start = 0
|
||||
}
|
||||
|
||||
end := line + context + 1
|
||||
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
}
|
||||
|
||||
return lines[start:end], initial
|
||||
}
|
||||
300
vendor/github.com/getsentry/sentry-go/stacktrace.go
generated
vendored
Normal file
300
vendor/github.com/getsentry/sentry-go/stacktrace.go
generated
vendored
Normal file
@ -0,0 +1,300 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const unknown string = "unknown"
|
||||
const contextLines int = 5
|
||||
|
||||
// The module download is split into two parts: downloading the go.mod and downloading the actual code.
|
||||
// If you have dependencies only needed for tests, then they will show up in your go.mod,
|
||||
// and go get will download their go.mods, but it will not download their code.
|
||||
// The test-only dependencies get downloaded only when you need it, such as the first time you run go test.
|
||||
//
|
||||
// https://github.com/golang/go/issues/26913#issuecomment-411976222
|
||||
|
||||
// Stacktrace holds information about the frames of the stack.
|
||||
type Stacktrace struct {
|
||||
Frames []Frame `json:"frames,omitempty"`
|
||||
FramesOmitted []uint `json:"frames_omitted,omitempty"`
|
||||
}
|
||||
|
||||
// NewStacktrace creates a stacktrace using `runtime.Callers`.
|
||||
func NewStacktrace() *Stacktrace {
|
||||
pcs := make([]uintptr, 100)
|
||||
n := runtime.Callers(1, pcs)
|
||||
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
frames := extractFrames(pcs[:n])
|
||||
frames = filterFrames(frames)
|
||||
frames = contextifyFrames(frames)
|
||||
|
||||
stacktrace := Stacktrace{
|
||||
Frames: frames,
|
||||
}
|
||||
|
||||
return &stacktrace
|
||||
}
|
||||
|
||||
// ExtractStacktrace creates a new `Stacktrace` based on the given `error` object.
|
||||
// TODO: Make it configurable so that anyone can provide their own implementation?
|
||||
// Use of reflection allows us to not have a hard dependency on any given package, so we don't have to import it
|
||||
func ExtractStacktrace(err error) *Stacktrace {
|
||||
method := extractReflectedStacktraceMethod(err)
|
||||
|
||||
if !method.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
pcs := extractPcs(method)
|
||||
|
||||
if len(pcs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
frames := extractFrames(pcs)
|
||||
frames = filterFrames(frames)
|
||||
frames = contextifyFrames(frames)
|
||||
|
||||
stacktrace := Stacktrace{
|
||||
Frames: frames,
|
||||
}
|
||||
|
||||
return &stacktrace
|
||||
}
|
||||
|
||||
func extractReflectedStacktraceMethod(err error) reflect.Value {
|
||||
var method reflect.Value
|
||||
|
||||
// https://github.com/pingcap/errors
|
||||
methodGetStackTracer := reflect.ValueOf(err).MethodByName("GetStackTracer")
|
||||
// https://github.com/pkg/errors
|
||||
methodStackTrace := reflect.ValueOf(err).MethodByName("StackTrace")
|
||||
// https://github.com/go-errors/errors
|
||||
methodStackFrames := reflect.ValueOf(err).MethodByName("StackFrames")
|
||||
|
||||
if methodGetStackTracer.IsValid() {
|
||||
stacktracer := methodGetStackTracer.Call(make([]reflect.Value, 0))[0]
|
||||
stacktracerStackTrace := reflect.ValueOf(stacktracer).MethodByName("StackTrace")
|
||||
|
||||
if stacktracerStackTrace.IsValid() {
|
||||
method = stacktracerStackTrace
|
||||
}
|
||||
}
|
||||
|
||||
if methodStackTrace.IsValid() {
|
||||
method = methodStackTrace
|
||||
}
|
||||
|
||||
if methodStackFrames.IsValid() {
|
||||
method = methodStackFrames
|
||||
}
|
||||
|
||||
return method
|
||||
}
|
||||
|
||||
func extractPcs(method reflect.Value) []uintptr {
|
||||
var pcs []uintptr
|
||||
|
||||
stacktrace := method.Call(make([]reflect.Value, 0))[0]
|
||||
|
||||
if stacktrace.Kind() != reflect.Slice {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < stacktrace.Len(); i++ {
|
||||
pc := stacktrace.Index(i)
|
||||
|
||||
if pc.Kind() == reflect.Uintptr {
|
||||
pcs = append(pcs, uintptr(pc.Uint()))
|
||||
continue
|
||||
}
|
||||
|
||||
if pc.Kind() == reflect.Struct {
|
||||
field := pc.FieldByName("ProgramCounter")
|
||||
if field.IsValid() && field.Kind() == reflect.Uintptr {
|
||||
pcs = append(pcs, uintptr(field.Uint()))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pcs
|
||||
}
|
||||
|
||||
// https://docs.sentry.io/development/sdk-dev/interfaces/stacktrace/
|
||||
type Frame struct {
|
||||
Function string `json:"function,omitempty"`
|
||||
Symbol string `json:"symbol,omitempty"`
|
||||
Module string `json:"module,omitempty"`
|
||||
Package string `json:"package,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
AbsPath string `json:"abs_path,omitempty"`
|
||||
Lineno int `json:"lineno,omitempty"`
|
||||
Colno int `json:"colno,omitempty"`
|
||||
PreContext []string `json:"pre_context,omitempty"`
|
||||
ContextLine string `json:"context_line,omitempty"`
|
||||
PostContext []string `json:"post_context,omitempty"`
|
||||
InApp bool `json:"in_app,omitempty"`
|
||||
Vars map[string]interface{} `json:"vars,omitempty"`
|
||||
}
|
||||
|
||||
// NewFrame assembles a stacktrace frame out of `runtime.Frame`.
|
||||
func NewFrame(f runtime.Frame) Frame {
|
||||
abspath := f.File
|
||||
filename := f.File
|
||||
function := f.Function
|
||||
var module string
|
||||
|
||||
if filename != "" {
|
||||
filename = extractFilename(filename)
|
||||
} else {
|
||||
filename = unknown
|
||||
}
|
||||
|
||||
if abspath == "" {
|
||||
abspath = unknown
|
||||
}
|
||||
|
||||
if function != "" {
|
||||
module, function = deconstructFunctionName(function)
|
||||
}
|
||||
|
||||
frame := Frame{
|
||||
AbsPath: abspath,
|
||||
Filename: filename,
|
||||
Lineno: f.Line,
|
||||
Module: module,
|
||||
Function: function,
|
||||
}
|
||||
|
||||
frame.InApp = isInAppFrame(frame)
|
||||
|
||||
return frame
|
||||
}
|
||||
|
||||
func extractFrames(pcs []uintptr) []Frame {
|
||||
var frames []Frame
|
||||
callersFrames := runtime.CallersFrames(pcs)
|
||||
|
||||
for {
|
||||
callerFrame, more := callersFrames.Next()
|
||||
|
||||
frames = append([]Frame{
|
||||
NewFrame(callerFrame),
|
||||
}, frames...)
|
||||
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return frames
|
||||
}
|
||||
|
||||
func filterFrames(frames []Frame) []Frame {
|
||||
isTestFileRegexp := regexp.MustCompile(`getsentry/sentry-go/.+_test.go`)
|
||||
isExampleFileRegexp := regexp.MustCompile(`getsentry/sentry-go/example/`)
|
||||
filteredFrames := make([]Frame, 0, len(frames))
|
||||
|
||||
for _, frame := range frames {
|
||||
// go runtime frames
|
||||
if frame.Module == "runtime" || frame.Module == "testing" {
|
||||
continue
|
||||
}
|
||||
// sentry internal frames
|
||||
isTestFile := isTestFileRegexp.MatchString(frame.AbsPath)
|
||||
isExampleFile := isExampleFileRegexp.MatchString(frame.AbsPath)
|
||||
if strings.Contains(frame.AbsPath, "github.com/getsentry/sentry-go") &&
|
||||
!isTestFile &&
|
||||
!isExampleFile {
|
||||
continue
|
||||
}
|
||||
filteredFrames = append(filteredFrames, frame)
|
||||
}
|
||||
|
||||
return filteredFrames
|
||||
}
|
||||
|
||||
var sr = newSourceReader() // nolint: gochecknoglobals
|
||||
|
||||
func contextifyFrames(frames []Frame) []Frame {
|
||||
contextifiedFrames := make([]Frame, 0, len(frames))
|
||||
|
||||
for _, frame := range frames {
|
||||
var path string
|
||||
|
||||
// If we are not able to read the source code from either absolute or relative path (root dir only)
|
||||
// Skip this part and return the original frame
|
||||
switch {
|
||||
case fileExists(frame.AbsPath):
|
||||
path = frame.AbsPath
|
||||
case fileExists(frame.Filename):
|
||||
path = frame.Filename
|
||||
default:
|
||||
contextifiedFrames = append(contextifiedFrames, frame)
|
||||
continue
|
||||
}
|
||||
|
||||
lines, initial := sr.readContextLines(path, frame.Lineno, contextLines)
|
||||
|
||||
for i, line := range lines {
|
||||
switch {
|
||||
case i < initial:
|
||||
frame.PreContext = append(frame.PreContext, string(line))
|
||||
case i == initial:
|
||||
frame.ContextLine = string(line)
|
||||
default:
|
||||
frame.PostContext = append(frame.PostContext, string(line))
|
||||
}
|
||||
}
|
||||
|
||||
contextifiedFrames = append(contextifiedFrames, frame)
|
||||
}
|
||||
|
||||
return contextifiedFrames
|
||||
}
|
||||
|
||||
func extractFilename(path string) string {
|
||||
_, file := filepath.Split(path)
|
||||
return file
|
||||
}
|
||||
|
||||
func isInAppFrame(frame Frame) bool {
|
||||
if frame.Module == "main" {
|
||||
return true
|
||||
}
|
||||
|
||||
if !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Transform `runtime/debug.*T·ptrmethod` into `{ module: runtime/debug, function: *T.ptrmethod }`
|
||||
func deconstructFunctionName(name string) (module string, function string) {
|
||||
if idx := strings.LastIndex(name, "."); idx != -1 {
|
||||
module = name[:idx]
|
||||
function = name[idx+1:]
|
||||
}
|
||||
function = strings.Replace(function, "·", ".", -1)
|
||||
return module, function
|
||||
}
|
||||
|
||||
func callerFunctionName() string {
|
||||
pcs := make([]uintptr, 1)
|
||||
runtime.Callers(3, pcs)
|
||||
callersFrames := runtime.CallersFrames(pcs)
|
||||
callerFrame, _ := callersFrames.Next()
|
||||
_, function := deconstructFunctionName(callerFrame.Function)
|
||||
return function
|
||||
}
|
||||
294
vendor/github.com/getsentry/sentry-go/transport.go
generated
vendored
Normal file
294
vendor/github.com/getsentry/sentry-go/transport.go
generated
vendored
Normal file
@ -0,0 +1,294 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultBufferSize = 30
|
||||
const defaultRetryAfter = time.Second * 60
|
||||
const defaultTimeout = time.Second * 30
|
||||
|
||||
// Transport is used by the `Client` to deliver events to remote server.
|
||||
type Transport interface {
|
||||
Flush(timeout time.Duration) bool
|
||||
Configure(options ClientOptions)
|
||||
SendEvent(event *Event)
|
||||
}
|
||||
|
||||
func getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error) {
|
||||
if options.HTTPSProxy != "" {
|
||||
return func(_ *http.Request) (*url.URL, error) {
|
||||
return url.Parse(options.HTTPSProxy)
|
||||
}
|
||||
} else if options.HTTPProxy != "" {
|
||||
return func(_ *http.Request) (*url.URL, error) {
|
||||
return url.Parse(options.HTTPProxy)
|
||||
}
|
||||
}
|
||||
|
||||
return http.ProxyFromEnvironment
|
||||
}
|
||||
|
||||
func getTLSConfig(options ClientOptions) *tls.Config {
|
||||
if options.CaCerts != nil {
|
||||
return &tls.Config{
|
||||
RootCAs: options.CaCerts,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func retryAfter(now time.Time, r *http.Response) time.Duration {
|
||||
retryAfterHeader := r.Header["Retry-After"]
|
||||
|
||||
if retryAfterHeader == nil {
|
||||
return defaultRetryAfter
|
||||
}
|
||||
|
||||
if date, err := time.Parse(time.RFC1123, retryAfterHeader[0]); err == nil {
|
||||
return date.Sub(now)
|
||||
}
|
||||
|
||||
if seconds, err := strconv.Atoi(retryAfterHeader[0]); err == nil {
|
||||
return time.Second * time.Duration(seconds)
|
||||
}
|
||||
|
||||
return defaultRetryAfter
|
||||
}
|
||||
|
||||
// ================================
|
||||
// HTTPTransport
|
||||
// ================================
|
||||
|
||||
// HTTPTransport is a default implementation of `Transport` interface used by `Client`.
|
||||
type HTTPTransport struct {
|
||||
dsn *Dsn
|
||||
client *http.Client
|
||||
transport *http.Transport
|
||||
|
||||
buffer chan *http.Request
|
||||
disabledUntil time.Time
|
||||
|
||||
wg sync.WaitGroup
|
||||
start sync.Once
|
||||
|
||||
// Size of the transport buffer. Defaults to 30.
|
||||
BufferSize int
|
||||
// HTTP Client request timeout. Defaults to 30 seconds.
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// NewHTTPTransport returns a new pre-configured instance of HTTPTransport
|
||||
func NewHTTPTransport() *HTTPTransport {
|
||||
transport := HTTPTransport{
|
||||
BufferSize: defaultBufferSize,
|
||||
Timeout: defaultTimeout,
|
||||
}
|
||||
return &transport
|
||||
}
|
||||
|
||||
// Configure is called by the `Client` itself, providing it it's own `ClientOptions`.
|
||||
func (t *HTTPTransport) Configure(options ClientOptions) {
|
||||
dsn, err := NewDsn(options.Dsn)
|
||||
if err != nil {
|
||||
Logger.Printf("%v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.dsn = dsn
|
||||
t.buffer = make(chan *http.Request, t.BufferSize)
|
||||
|
||||
if options.HTTPTransport != nil {
|
||||
t.transport = options.HTTPTransport
|
||||
} else {
|
||||
t.transport = &http.Transport{
|
||||
Proxy: getProxyConfig(options),
|
||||
TLSClientConfig: getTLSConfig(options),
|
||||
}
|
||||
}
|
||||
|
||||
t.client = &http.Client{
|
||||
Transport: t.transport,
|
||||
Timeout: t.Timeout,
|
||||
}
|
||||
|
||||
t.start.Do(func() {
|
||||
go t.worker()
|
||||
})
|
||||
}
|
||||
|
||||
// SendEvent assembles a new packet out of `Event` and sends it to remote server.
|
||||
func (t *HTTPTransport) SendEvent(event *Event) {
|
||||
if t.dsn == nil || time.Now().Before(t.disabledUntil) {
|
||||
return
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
request, _ := http.NewRequest(
|
||||
http.MethodPost,
|
||||
t.dsn.StoreAPIURL().String(),
|
||||
bytes.NewBuffer(body),
|
||||
)
|
||||
|
||||
for headerKey, headerValue := range t.dsn.RequestHeaders() {
|
||||
request.Header.Set(headerKey, headerValue)
|
||||
}
|
||||
|
||||
select {
|
||||
case t.buffer <- request:
|
||||
Logger.Printf(
|
||||
"Sending %s event [%s] to %s project: %d\n",
|
||||
event.Level,
|
||||
event.EventID,
|
||||
t.dsn.host,
|
||||
t.dsn.projectID,
|
||||
)
|
||||
t.wg.Add(1)
|
||||
default:
|
||||
Logger.Println("Event dropped due to transport buffer being full")
|
||||
// worker would block, drop the packet
|
||||
}
|
||||
}
|
||||
|
||||
// Flush notifies when all the buffered events have been sent by returning `true`
|
||||
// or `false` if timeout was reached.
|
||||
func (t *HTTPTransport) Flush(timeout time.Duration) bool {
|
||||
c := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
t.wg.Wait()
|
||||
close(c)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-c:
|
||||
Logger.Println("Buffer flushed successfully")
|
||||
return true
|
||||
case <-time.After(timeout):
|
||||
Logger.Println("Buffer flushing reached the timeout")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HTTPTransport) worker() {
|
||||
for request := range t.buffer {
|
||||
if time.Now().Before(t.disabledUntil) {
|
||||
t.wg.Done()
|
||||
continue
|
||||
}
|
||||
|
||||
response, err := t.client.Do(request)
|
||||
|
||||
if err != nil {
|
||||
Logger.Printf("There was an issue with sending an event: %v", err)
|
||||
}
|
||||
|
||||
if response != nil && response.StatusCode == http.StatusTooManyRequests {
|
||||
t.disabledUntil = time.Now().Add(retryAfter(time.Now(), response))
|
||||
Logger.Printf("Too many requests, backing off till: %s\n", t.disabledUntil)
|
||||
}
|
||||
|
||||
t.wg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// HTTPSyncTransport
|
||||
// ================================
|
||||
|
||||
// HTTPSyncTransport is an implementation of `Transport` interface which blocks after each captured event.
|
||||
type HTTPSyncTransport struct {
|
||||
dsn *Dsn
|
||||
client *http.Client
|
||||
transport *http.Transport
|
||||
disabledUntil time.Time
|
||||
|
||||
// HTTP Client request timeout. Defaults to 30 seconds.
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// NewHTTPSyncTransport returns a new pre-configured instance of HTTPSyncTransport
|
||||
func NewHTTPSyncTransport() *HTTPSyncTransport {
|
||||
transport := HTTPSyncTransport{
|
||||
Timeout: defaultTimeout,
|
||||
}
|
||||
|
||||
return &transport
|
||||
}
|
||||
|
||||
// Configure is called by the `Client` itself, providing it it's own `ClientOptions`.
|
||||
func (t *HTTPSyncTransport) Configure(options ClientOptions) {
|
||||
dsn, err := NewDsn(options.Dsn)
|
||||
if err != nil {
|
||||
Logger.Printf("%v\n", err)
|
||||
return
|
||||
}
|
||||
t.dsn = dsn
|
||||
|
||||
if options.HTTPTransport != nil {
|
||||
t.transport = options.HTTPTransport
|
||||
} else {
|
||||
t.transport = &http.Transport{
|
||||
Proxy: getProxyConfig(options),
|
||||
TLSClientConfig: getTLSConfig(options),
|
||||
}
|
||||
}
|
||||
|
||||
t.client = &http.Client{
|
||||
Transport: t.transport,
|
||||
Timeout: t.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// SendEvent assembles a new packet out of `Event` and sends it to remote server.
|
||||
func (t *HTTPSyncTransport) SendEvent(event *Event) {
|
||||
if t.dsn == nil || time.Now().Before(t.disabledUntil) {
|
||||
return
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
request, _ := http.NewRequest(
|
||||
http.MethodPost,
|
||||
t.dsn.StoreAPIURL().String(),
|
||||
bytes.NewBuffer(body),
|
||||
)
|
||||
|
||||
for headerKey, headerValue := range t.dsn.RequestHeaders() {
|
||||
request.Header.Set(headerKey, headerValue)
|
||||
}
|
||||
|
||||
Logger.Printf(
|
||||
"Sending %s event [%s] to %s project: %d\n",
|
||||
event.Level,
|
||||
event.EventID,
|
||||
t.dsn.host,
|
||||
t.dsn.projectID,
|
||||
)
|
||||
|
||||
response, err := t.client.Do(request)
|
||||
|
||||
if err != nil {
|
||||
Logger.Printf("There was an issue with sending an event: %v", err)
|
||||
}
|
||||
|
||||
if response != nil && response.StatusCode == http.StatusTooManyRequests {
|
||||
t.disabledUntil = time.Now().Add(retryAfter(time.Now(), response))
|
||||
Logger.Printf("Too many requests, backing off till: %s\n", t.disabledUntil)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush notifies when all the buffered events have been sent by returning `true`
|
||||
// or `false` if timeout was reached. No-op for HTTPSyncTransport.
|
||||
func (t *HTTPSyncTransport) Flush(_ time.Duration) bool {
|
||||
return true
|
||||
}
|
||||
34
vendor/github.com/getsentry/sentry-go/util.go
generated
vendored
Normal file
34
vendor/github.com/getsentry/sentry-go/util.go
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func uuid() string {
|
||||
id := make([]byte, 16)
|
||||
_, _ = io.ReadFull(rand.Reader, id)
|
||||
id[6] &= 0x0F // clear version
|
||||
id[6] |= 0x40 // set version to 4 (random uuid)
|
||||
id[8] &= 0x3F // clear variant
|
||||
id[8] |= 0x80 // set to IETF variant
|
||||
return hex.EncodeToString(id)
|
||||
}
|
||||
|
||||
func fileExists(fileName string) bool {
|
||||
if _, err := os.Stat(fileName); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// nolint: deadcode, unused
|
||||
func prettyPrint(data interface{}) {
|
||||
dbg, _ := json.MarshalIndent(data, "", " ")
|
||||
fmt.Println(string(dbg))
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user