Skip to content
Snippets Groups Projects
sentry.go 4.24 KiB
Newer Older
Christophe de Vienne's avatar
Christophe de Vienne committed
package orusapi

import (
	"bytes"
Axel Prel's avatar
Axel Prel committed
	"errors"
Christophe de Vienne's avatar
Christophe de Vienne committed
	"io"

	"github.com/getsentry/sentry-go"
	jsoniter "github.com/json-iterator/go"
Axel Prel's avatar
Axel Prel committed
	"github.com/rs/zerolog"
Axel Prel's avatar
Axel Prel committed
var ErrNotAString = errors.New("not a string")

Christophe de Vienne's avatar
Christophe de Vienne committed
// SentryOptions ...
type SentryOptions struct {
	SentryDSN func(string) `long:"dsn" env:"DSN" ini-name:"dsn" description:"Sentry DSN"`
Axel Prel's avatar
Axel Prel committed
	TagNames  []string     `long:"tag-names" env:"TAG_NAMES" ini-name:"tag-names" description:"log properties that should be set as tags on the sentry event"` //nolint:lll
// NewSentryOptions ...
func NewSentryOptions(loggingOptions *LoggingOptions) *SentryOptions {
	sentryOptions := SentryOptions{}
	sentryOptions.SentryDSN = func(dsn string) {
		log := loggingOptions.Logger()
		client, err := sentry.NewClient(sentry.ClientOptions{
			Dsn: dsn,
		})
		if err != nil {
			log.Err(err).Msg("Could not initialize sentry")
Axel Prel's avatar
Axel Prel committed

			return
		}
		hub := sentry.NewHub(client, sentry.NewScope())
		loggingOptions.AddLogWrapper(
			func(next io.Writer) io.Writer {
				return SentryLogger{hub, next, &sentryOptions}
Axel Prel's avatar
Axel Prel committed
		// hub.Flush(time.Second)
		// })
Axel Prel's avatar
Axel Prel committed

	return &sentryOptions
Christophe de Vienne's avatar
Christophe de Vienne committed
}

// SentryLogger ...
type SentryLogger struct {
	hub     *sentry.Hub
	next    io.Writer
	options *SentryOptions
Christophe de Vienne's avatar
Christophe de Vienne committed
}

var (
	levelMarker       = []byte(`"level":"`)
	levelMarkerLen    = len(levelMarker)
	levelsFirstLetter = []byte(`wefp`)
)

func buildEventIfLevelGtWarn(tagNames []string, p []byte) *sentry.Event {
Christophe de Vienne's avatar
Christophe de Vienne committed
	if i := bytes.Index(p, levelMarker); i != -1 {
		first := p[i+levelMarkerLen]
		if bytes.IndexByte(levelsFirstLetter, first) != -1 {
			return buildEvent(tagNames, p)
Axel Prel's avatar
Axel Prel committed

Christophe de Vienne's avatar
Christophe de Vienne committed
	return nil
}

func buildEvent(tagNames []string, p []byte) *sentry.Event {
Christophe de Vienne's avatar
Christophe de Vienne committed
	iter := jsoniter.ConfigFastest.BorrowIterator(p)
	defer jsoniter.ConfigFastest.ReturnIterator(iter)

	if iter.WhatIsNext() != jsoniter.ObjectValue {
		return nil
	}
	event := sentry.NewEvent()

	if !iter.ReadObjectCB(func(iter *jsoniter.Iterator, field string) bool {
		switch field {
		case "message":
			event.Message = iter.ReadString()
		case "error":
			errMsg := iter.ReadString()
			if event.Message == "" {
				event.Message = errMsg
			}
			event.Extra["error"] = errMsg
		case "level":
			level := iter.ReadString()
Axel Prel's avatar
Axel Prel committed
			if level == zerolog.LevelWarnValue {
Christophe de Vienne's avatar
Christophe de Vienne committed
				event.Level = sentry.LevelWarning
			} else {
				event.Level = sentry.Level(level)
			}
		case "request":
			iter.ReadVal(&event.Request)
		case "user":
			if !iter.ReadObjectCB(func(iter *jsoniter.Iterator, field string) bool {
				switch field {
				case "id":
					event.User.ID = iter.ReadString()
				default:
					event.Extra["user."+field] = iter.Read()
				}
Axel Prel's avatar
Axel Prel committed

Christophe de Vienne's avatar
Christophe de Vienne committed
				return true
			}) {
				return false
			}
		case "exception":
			var e sentry.Exception
			iter.ReadVal(&e)
			event.Exception = []sentry.Exception{e}
		default:
			for _, tag := range tagNames {
				if tag == field {
					rawvalue := iter.Read()
					var value string
					if s, ok := rawvalue.(string); ok {
						value = s
					} else {
						value = fmt.Sprint(rawvalue)
					}
					if len(value) > 200 {
						value = value[:200]
					}
					event.Tags[tag] = value
Axel Prel's avatar
Axel Prel committed

Christophe de Vienne's avatar
Christophe de Vienne committed
			event.Extra[field] = iter.Read()
		}
Axel Prel's avatar
Axel Prel committed

Christophe de Vienne's avatar
Christophe de Vienne committed
		return true
	}) {
		return nil
	}
	if op, ok := event.Extra["api-operation-id"]; ok {
Axel Prel's avatar
Axel Prel committed
		if strOp, ok := op.(string); ok {
			event.Message = MustExtraGetStringD(event.Extra, "api-tag", "") + "/" + strOp + ": " + event.Message
		} else {
			panic("operation is not a string")
		}
Christophe de Vienne's avatar
Christophe de Vienne committed
	} else if event.Request != nil && event.Request.Method != "" {
		event.Message = event.Request.Method + " " + event.Request.URL + ": " + event.Message
	}
Axel Prel's avatar
Axel Prel committed

Christophe de Vienne's avatar
Christophe de Vienne committed
	return event
}

Axel Prel's avatar
Axel Prel committed
func MustExtraGetStringD(extra map[string]interface{}, key, defaultVal string) string {
	str, err := ExtraGetStringD(extra, key, defaultVal)
	if err != nil {
		panic(err)
	}

	return str
}

func ExtraGetStringD(extra map[string]interface{}, key, defaultVal string) (string, error) {
	if value, ok := extra[key]; ok {
		if strValue, ok := value.(string); ok {
			return strValue, nil
		}

		return "", fmt.Errorf("error %w: value %#v", ErrNotAString, value)
	}

	return defaultVal, nil
}

Christophe de Vienne's avatar
Christophe de Vienne committed
// Write ...
Axel Prel's avatar
Axel Prel committed
func (l SentryLogger) Write(p []byte) (int, error) {
	if event := buildEventIfLevelGtWarn(l.options.TagNames, p); event != nil {
Christophe de Vienne's avatar
Christophe de Vienne committed
		l.hub.CaptureEvent(event)
	}
Axel Prel's avatar
Axel Prel committed

Christophe de Vienne's avatar
Christophe de Vienne committed
	return l.next.Write(p)
}