package orusapi import ( "bytes" "errors" "fmt" "io" "github.com/getsentry/sentry-go" jsoniter "github.com/json-iterator/go" "github.com/rs/zerolog" ) var ErrNotAString = errors.New("not a string") // SentryOptions ... type SentryOptions struct { SentryDSN func(string) `long:"dsn" env:"DSN" ini-name:"dsn" description:"Sentry DSN"` 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") return } hub := sentry.NewHub(client, sentry.NewScope()) loggingOptions.AddLogWrapper( func(next io.Writer) io.Writer { return SentryLogger{hub, next, &sentryOptions} }) // OnShutdown(func() { // hub.Flush(time.Second) // }) } return &sentryOptions } // SentryLogger ... type SentryLogger struct { hub *sentry.Hub next io.Writer options *SentryOptions } var ( levelMarker = []byte(`"level":"`) levelMarkerLen = len(levelMarker) levelsFirstLetter = []byte(`wefp`) ) func buildEventIfLevelGtWarn(tagNames []string, p []byte) *sentry.Event { if i := bytes.Index(p, levelMarker); i != -1 { first := p[i+levelMarkerLen] if bytes.IndexByte(levelsFirstLetter, first) != -1 { return buildEvent(tagNames, p) } } return nil } func buildEvent(tagNames []string, p []byte) *sentry.Event { 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() if level == zerolog.LevelWarnValue { 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() } 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 return true } } event.Extra[field] = iter.Read() } return true }) { return nil } if op, ok := event.Extra["api-operation-id"]; ok { if strOp, ok := op.(string); ok { event.Message = MustExtraGetStringD(event.Extra, "api-tag", "") + "/" + strOp + ": " + event.Message } else { panic("operation is not a string") } } else if event.Request != nil && event.Request.Method != "" { event.Message = event.Request.Method + " " + event.Request.URL + ": " + event.Message } return event } 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 } // Write ... func (l SentryLogger) Write(p []byte) (int, error) { if event := buildEventIfLevelGtWarn(l.options.TagNames, p); event != nil { l.hub.CaptureEvent(event) } return l.next.Write(p) }