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 }