github.com/facebookincubator/go-belt@v0.0.0-20230703220935-39cd348f1a38/tool/experimental/errmon/implementation/sentry/errmon.go (about)

     1  // Copyright 2022 Meta Platforms, Inc. and affiliates.
     2  //
     3  // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
     4  //
     5  // 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
     6  //
     7  // 2. 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.
     8  //
     9  // 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
    10  //
    11  // 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.
    12  
    13  package sentry
    14  
    15  import (
    16  	"fmt"
    17  	"strings"
    18  
    19  	"github.com/facebookincubator/go-belt/pkg/field"
    20  	"github.com/facebookincubator/go-belt/pkg/runtime"
    21  	"github.com/facebookincubator/go-belt/tool/experimental/errmon"
    22  	errmonadapter "github.com/facebookincubator/go-belt/tool/experimental/errmon/adapter"
    23  	errmontypes "github.com/facebookincubator/go-belt/tool/experimental/errmon/types"
    24  	"github.com/facebookincubator/go-belt/tool/experimental/tracer"
    25  	loggertypes "github.com/facebookincubator/go-belt/tool/logger/types"
    26  	"github.com/getsentry/sentry-go"
    27  )
    28  
    29  // Emitter is a wrapper for a Sentry client to implement errmon.Emitter.
    30  type Emitter struct {
    31  	SentryClient *sentry.Client
    32  }
    33  
    34  // NewEmitter returns a new instance of Emitter.
    35  func NewEmitter(sentryClient *sentry.Client) *Emitter {
    36  	return &Emitter{
    37  		SentryClient: sentryClient,
    38  	}
    39  }
    40  
    41  // New wraps a Sentry client and returns a new instance, which implements errmon.ErrorMonitor.
    42  func New(
    43  	sentryClient *sentry.Client,
    44  	opts ...Option,
    45  ) errmon.ErrorMonitor {
    46  	return errmonadapter.ErrorMonitorFromEmitter(
    47  		NewEmitter(sentryClient),
    48  		options(opts).Config().CallerFrameFilter,
    49  	)
    50  }
    51  
    52  // Flush implements errmon.Emitter
    53  func (*Emitter) Flush() {}
    54  
    55  // Emit implements errmon.Emitter.
    56  func (h *Emitter) Emit(ev *errmon.Event) {
    57  	sendEvent := EventToSentry(ev)
    58  
    59  	eventID := h.SentryClient.CaptureEvent(sendEvent, nil, nil)
    60  	if eventID == nil {
    61  		return
    62  	}
    63  
    64  	ev.ExternalIDs = append(ev.ExternalIDs, eventID)
    65  }
    66  
    67  // LevelToSentry returns the closest sentry analog of a given logger.Level
    68  func LevelToSentry(level loggertypes.Level) sentry.Level {
    69  	switch level {
    70  	case loggertypes.LevelTrace, loggertypes.LevelDebug:
    71  		return sentry.LevelDebug
    72  	case loggertypes.LevelInfo:
    73  		return sentry.LevelInfo
    74  	case loggertypes.LevelWarning:
    75  		return sentry.LevelWarning
    76  	case loggertypes.LevelError:
    77  		return sentry.LevelError
    78  	case loggertypes.LevelPanic, loggertypes.LevelFatal:
    79  		return sentry.LevelFatal
    80  	default:
    81  		return sentry.LevelError
    82  	}
    83  }
    84  
    85  // FuncNameToSentryModule converts a funcation name (see runtime.Frame) to
    86  // a sentry module name.
    87  func FuncNameToSentryModule(funcName string) string {
    88  	return strings.Split(funcName, ".")[0]
    89  }
    90  
    91  // GoroutinesToSentry converts goroutines to the Sentry format.
    92  func GoroutinesToSentry(goroutines []errmontypes.Goroutine, currentGoroutineID int) []sentry.Thread {
    93  	result := make([]sentry.Thread, 0, len(goroutines))
    94  	for _, goroutine := range goroutines {
    95  		converted := sentry.Thread{
    96  			ID:      fmt.Sprint(goroutine.ID),
    97  			Current: goroutine.ID == currentGoroutineID,
    98  			Stacktrace: &sentry.Stacktrace{
    99  				Frames: make([]sentry.Frame, 0, len(goroutine.Stack)),
   100  			},
   101  		}
   102  		if goroutine.LockedToThread {
   103  			converted.Name = fmt.Sprintf("goroutine_lockedToThread_%08X", goroutine.ID)
   104  		} else {
   105  			converted.Name = fmt.Sprintf("goroutine_%08X", goroutine.ID)
   106  		}
   107  		for _, frame := range goroutine.Stack {
   108  			if frame.Func == "panic" && strings.HasSuffix(frame.File, "runtime/panic.go") {
   109  				converted.Crashed = true
   110  			}
   111  
   112  			converted.Stacktrace.Frames = append(converted.Stacktrace.Frames, sentry.Frame{
   113  				Function: frame.Func,
   114  				Filename: frame.File,
   115  				Lineno:   frame.Line,
   116  				Module:   FuncNameToSentryModule(frame.Func),
   117  			})
   118  		}
   119  
   120  		result = append(result, converted)
   121  	}
   122  	return result
   123  }
   124  
   125  // StackTraceToSentry converts a stack trace to the Sentry format.
   126  func StackTraceToSentry(stackTrace runtime.StackTrace) *sentry.Stacktrace {
   127  	frames := stackTrace.Frames()
   128  	if frames == nil {
   129  		return nil
   130  	}
   131  	result := &sentry.Stacktrace{
   132  		Frames: make([]sentry.Frame, 0, stackTrace.Len()),
   133  	}
   134  	for {
   135  		frame, ok := frames.Next()
   136  		result.Frames = append(result.Frames, sentry.Frame{
   137  			Function: frame.Function,
   138  			Module:   FuncNameToSentryModule(frame.Function),
   139  			Filename: frame.File,
   140  			Lineno:   frame.Line,
   141  		})
   142  		if !ok {
   143  			break
   144  		}
   145  	}
   146  	return result
   147  }
   148  
   149  // SpansToSentry converts tracer spans to the Sentry format.
   150  func SpansToSentry(spans tracer.Spans) []*sentry.Span {
   151  	var result []*sentry.Span
   152  	for _, span := range spans {
   153  		if tracer.IsNoopSpan(span) {
   154  			continue
   155  		}
   156  		entry := &sentry.Span{
   157  			StartTime:   span.StartTS(),
   158  			Description: span.Name(),
   159  			Status:      sentry.SpanStatusOK,
   160  			Data:        map[string]interface{}{},
   161  		}
   162  		span.Fields().ForEachField(func(f *field.Field) bool {
   163  			entry.Data[f.Key] = f.Value
   164  			return true
   165  		})
   166  		traceIDs := span.TraceIDs()
   167  		if len(traceIDs) > 0 {
   168  			copy(entry.TraceID[:], traceIDs[0])
   169  		}
   170  		copy(entry.SpanID[:], fmt.Sprint(span.ID()))
   171  		if parent := span.Parent(); parent != nil {
   172  			copy(entry.ParentSpanID[:], fmt.Sprint(parent.ID()))
   173  		}
   174  		result = append(result, entry)
   175  	}
   176  	return result
   177  }
   178  
   179  // UserToSentry converts an user structure to the Sentry format.
   180  func UserToSentry(user *errmontypes.User) sentry.User {
   181  	result := sentry.User{
   182  		ID:       fmt.Sprint(user.ID),
   183  		Username: user.Name,
   184  	}
   185  
   186  	for _, v := range user.CustomData {
   187  		switch v := v.(type) {
   188  		case UserEmail:
   189  			result.Email = string(v)
   190  		case UserIPAddress:
   191  			result.IPAddress = string(v)
   192  		}
   193  	}
   194  
   195  	return result
   196  }
   197  
   198  // HTTPRequestToSentry converts HTTP request info to the Sentry format.
   199  func HTTPRequestToSentry(request *errmon.HTTPRequest) *sentry.Request {
   200  	headers := make(map[string]string, len(request.Header))
   201  	for name, values := range request.Header {
   202  		headers[name] = strings.Join(values, "\n")
   203  	}
   204  	return &sentry.Request{
   205  		URL:         request.URL.String(),
   206  		Method:      request.Method,
   207  		QueryString: request.URL.RawQuery,
   208  		Cookies:     headers["Cookie"],
   209  		Headers:     headers,
   210  	}
   211  }
   212  
   213  // BreadcrumbToSentry converts a Breadcrumb to the Sentry format.
   214  func BreadcrumbToSentry(breadcrumb *errmontypes.Breadcrumb) *sentry.Breadcrumb {
   215  	data := map[string]interface{}{}
   216  	breadcrumb.ForEachField(func(f *field.Field) bool {
   217  		data[f.Key] = f.Value
   218  		return true
   219  	})
   220  	return &sentry.Breadcrumb{
   221  		Type:      strings.Join(breadcrumb.Path, "."),
   222  		Category:  strings.Join(breadcrumb.Categories, ","),
   223  		Data:      data,
   224  		Timestamp: breadcrumb.TS,
   225  	}
   226  }
   227  
   228  // PackageToSentry converts a Package to the Sentry format.
   229  func PackageToSentry(pkg *errmontypes.Package) sentry.SdkPackage {
   230  	return sentry.SdkPackage{
   231  		Name:    pkg.Name,
   232  		Version: pkg.Version,
   233  	}
   234  }
   235  
   236  // EventToSentry converts an Event to the Sentry format.
   237  func EventToSentry(ev *errmontypes.Event) *sentry.Event {
   238  	result := &sentry.Event{
   239  		EventID:  sentry.EventID(ev.ID),
   240  		Level:    LevelToSentry(ev.Level),
   241  		Message:  ev.Message,
   242  		Platform: "go",
   243  		Sdk: sentry.SdkInfo{
   244  			Name: "go-belt",
   245  		},
   246  		Threads:   GoroutinesToSentry(ev.Goroutines, ev.CurrentGoroutineID),
   247  		Timestamp: ev.Timestamp,
   248  
   249  		Tags:    map[string]string{},
   250  		Modules: map[string]string{},
   251  		Extra:   map[string]interface{}{},
   252  
   253  		StartTime: ev.Spans.Earliest().StartTS(),
   254  		Spans:     SpansToSentry(ev.Spans),
   255  	}
   256  
   257  	if ev.Error != nil {
   258  		result.Exception = append(result.Exception, sentry.Exception{
   259  			Type:       "error",
   260  			Value:      ev.Error.Error(),
   261  			Module:     FuncNameToSentryModule(ev.Caller.Func().Name()),
   262  			ThreadID:   "goroutine",
   263  			Stacktrace: StackTraceToSentry(ev.StackTrace),
   264  		})
   265  	}
   266  	if ev.IsPanic {
   267  		result.Exception = append(result.Exception, sentry.Exception{
   268  			Type:       "panic",
   269  			Value:      fmt.Sprint(ev.PanicValue),
   270  			Module:     FuncNameToSentryModule(ev.Caller.Func().Name()),
   271  			ThreadID:   "goroutine",
   272  			Stacktrace: StackTraceToSentry(ev.StackTrace),
   273  		})
   274  	}
   275  
   276  	observeField := func(f *field.Field) bool {
   277  		switch value := f.Value.(type) {
   278  		case errmontypes.User:
   279  			result.User = UserToSentry(&value)
   280  		case *errmontypes.User:
   281  			result.User = UserToSentry(value)
   282  		case errmontypes.HTTPRequest:
   283  			result.Request = HTTPRequestToSentry(&value)
   284  		case *errmontypes.HTTPRequest:
   285  			result.Request = HTTPRequestToSentry(value)
   286  		case errmontypes.Breadcrumb:
   287  			result.Breadcrumbs = append(result.Breadcrumbs, BreadcrumbToSentry(&value))
   288  		case *errmontypes.Breadcrumb:
   289  			result.Breadcrumbs = append(result.Breadcrumbs, BreadcrumbToSentry(value))
   290  		case errmontypes.Package:
   291  			result.Sdk.Packages = append(result.Sdk.Packages, PackageToSentry(&value))
   292  		case *errmontypes.Package:
   293  			result.Sdk.Packages = append(result.Sdk.Packages, PackageToSentry(value))
   294  		case errmontypes.Tag:
   295  			result.Tags[value.Key] = value.Value
   296  		case *errmontypes.Tag:
   297  			result.Tags[value.Key] = value.Value
   298  		case tracer.SpanOptionRole:
   299  			result.Type = string(value)
   300  		case *tracer.SpanOptionRole:
   301  			result.Type = string(*value)
   302  		default:
   303  			switch {
   304  			case f.Properties.Has(errmontypes.FieldPropEnvironment):
   305  				result.Environment = fmt.Sprint(f.Value)
   306  			case f.Properties.Has(errmontypes.FieldPropRelease):
   307  				result.Release = fmt.Sprint(f.Value)
   308  			case f.Properties.Has(errmontypes.FieldPropServerName):
   309  				result.ServerName = fmt.Sprint(f.Value)
   310  			default:
   311  				result.Extra[f.Key] = f.Value
   312  			}
   313  		}
   314  
   315  		return true
   316  	}
   317  
   318  	ev.Fields.ForEachField(observeField)
   319  	return result
   320  }