github.com/blend/go-sdk@v1.20220411.3/sentry/client.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package sentry
     9  
    10  import (
    11  	"context"
    12  	"fmt"
    13  	"net"
    14  	"net/http"
    15  	"os"
    16  	"runtime"
    17  
    18  	raven "github.com/blend/sentry-go"
    19  
    20  	"github.com/blend/go-sdk/env"
    21  	"github.com/blend/go-sdk/ex"
    22  	"github.com/blend/go-sdk/logger"
    23  	"github.com/blend/go-sdk/webutil"
    24  )
    25  
    26  var (
    27  	_ Sender = (*Client)(nil)
    28  )
    29  
    30  // MustNew returns a new client and panics on error.
    31  func MustNew(cfg Config) *Client {
    32  	c, err := New(cfg)
    33  	if err != nil {
    34  		panic(err)
    35  	}
    36  	return c
    37  }
    38  
    39  // New returns a new client.
    40  func New(cfg Config) (*Client, error) {
    41  	rc, err := raven.NewClient(
    42  		raven.ClientOptions{
    43  			Dsn:         cfg.DSN,
    44  			Environment: cfg.Environment,
    45  			ServerName:  cfg.ServerName,
    46  			Dist:        cfg.Dist,
    47  			Release:     cfg.Release,
    48  			Debug:       cfg.Debug,
    49  			DebugWriter: os.Stderr,
    50  		},
    51  	)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	return &Client{
    56  		Config: cfg,
    57  		Client: rc,
    58  	}, nil
    59  }
    60  
    61  // Client is a wrapper for the sentry-go client.
    62  type Client struct {
    63  	Config Config
    64  	Client *raven.Client
    65  }
    66  
    67  // Notify sends a notification.
    68  func (c Client) Notify(ctx context.Context, ee logger.ErrorEvent) {
    69  	c.Client.CaptureEvent(errEvent(ctx, ee), nil, raven.NewScope())
    70  	c.Client.Flush(c.Config.FlushTimeoutOrDefault()) // goose this a bit
    71  }
    72  
    73  func errEvent(ctx context.Context, ee logger.ErrorEvent) *raven.Event {
    74  	exceptions := []raven.Exception{
    75  		{
    76  			Type:       ex.ErrClass(ee.Err).Error(),
    77  			Value:      ex.ErrMessage(ee.Err),
    78  			Stacktrace: errStackTrace(ee.Err),
    79  		},
    80  	}
    81  
    82  	var innerErr error
    83  	for innerErr = ex.ErrInner(ee.Err); innerErr != nil; innerErr = ex.ErrInner(innerErr) {
    84  		rex := raven.Exception{
    85  			Type:  ex.ErrClass(innerErr).Error(),
    86  			Value: ex.ErrMessage(innerErr),
    87  		}
    88  		if st := errStackTrace(innerErr); st != nil && len(st.Frames) > 0 {
    89  			rex.Stacktrace = st
    90  		}
    91  		exceptions = append(exceptions, rex)
    92  	}
    93  
    94  	event := &raven.Event{
    95  		Title:       ex.ErrClass(ee.Err).Error(),
    96  		Timestamp:   logger.GetEventTimestamp(ctx, ee),
    97  		Fingerprint: errFingerprint(ctx, ex.ErrClass(ee.Err).Error()),
    98  		Level:       raven.Level(ee.GetFlag()),
    99  		Tags:        errTags(ctx),
   100  		Extra:       errExtra(ctx),
   101  		Platform:    "go",
   102  		Contexts: map[string]interface{}{
   103  			"device": map[string]interface{}{
   104  				"arch":    runtime.GOARCH,
   105  				"num_cpu": runtime.NumCPU(),
   106  			},
   107  			"os": map[string]interface{}{
   108  				"name": runtime.GOOS,
   109  			},
   110  			"runtime": map[string]interface{}{
   111  				"name":           "go",
   112  				"version":        runtime.Version(),
   113  				"go_numroutines": runtime.NumGoroutine(),
   114  				"go_maxprocs":    runtime.GOMAXPROCS(0),
   115  				"go_numcgocalls": runtime.NumCgoCall(),
   116  			},
   117  		},
   118  		Sdk: raven.SdkInfo{
   119  			Name:    SDK,
   120  			Version: raven.Version,
   121  			Packages: []raven.SdkPackage{{
   122  				Name:    SDK,
   123  				Version: raven.Version,
   124  			}},
   125  		},
   126  		Request:   errRequest(ee),
   127  		Message:   ex.ErrClass(ee.Err).Error(),
   128  		Exception: exceptions,
   129  	}
   130  
   131  	// Set contextual information preserving existing data. For each context, if
   132  	// the existing value is not of type map[string]interface{}, then no
   133  	// additional information is added.
   134  	if deviceContext, ok := event.Contexts["device"].(map[string]interface{}); ok {
   135  		if _, ok := deviceContext["arch"]; !ok {
   136  			deviceContext["arch"] = runtime.GOARCH
   137  		}
   138  		if _, ok := deviceContext["num_cpu"]; !ok {
   139  			deviceContext["num_cpu"] = runtime.NumCPU()
   140  		}
   141  	}
   142  	if osContext, ok := event.Contexts["os"].(map[string]interface{}); ok {
   143  		if _, ok := osContext["name"]; !ok {
   144  			osContext["name"] = runtime.GOOS
   145  		}
   146  	}
   147  	if runtimeContext, ok := event.Contexts["runtime"].(map[string]interface{}); ok {
   148  		if _, ok := runtimeContext["name"]; !ok {
   149  			runtimeContext["name"] = "go"
   150  		}
   151  		if _, ok := runtimeContext["version"]; !ok {
   152  			runtimeContext["version"] = runtime.Version()
   153  		}
   154  		if _, ok := runtimeContext["go_numroutines"]; !ok {
   155  			runtimeContext["go_numroutines"] = runtime.NumGoroutine()
   156  		}
   157  		if _, ok := runtimeContext["go_maxprocs"]; !ok {
   158  			runtimeContext["go_maxprocs"] = runtime.GOMAXPROCS(0)
   159  		}
   160  		if _, ok := runtimeContext["go_numcgocalls"]; !ok {
   161  			runtimeContext["go_numcgocalls"] = runtime.NumCgoCall()
   162  		}
   163  	}
   164  
   165  	return event
   166  }
   167  
   168  func errFingerprint(ctx context.Context, extra ...string) []string {
   169  	if fingerprint := GetFingerprint(ctx); fingerprint != nil {
   170  		return fingerprint
   171  	}
   172  	return extra
   173  }
   174  
   175  func errTags(ctx context.Context) map[string]string {
   176  	labels := logger.GetLabels(ctx)
   177  	if labels == nil {
   178  		labels = make(map[string]string)
   179  	}
   180  	if hostname := env.GetVars(ctx).Hostname(); hostname != "" {
   181  		labels["hostname"] = hostname
   182  	}
   183  	return labels
   184  }
   185  
   186  func errExtra(ctx context.Context) map[string]interface{} {
   187  	return logger.GetAnnotations(ctx)
   188  }
   189  
   190  func errRequest(ee logger.ErrorEvent) *raven.Request {
   191  	if ee.State == nil {
   192  		return new(raven.Request)
   193  	}
   194  	typed, ok := ee.State.(*http.Request)
   195  	if !ok {
   196  		return &raven.Request{}
   197  	}
   198  
   199  	return newRavenRequest(typed)
   200  }
   201  
   202  func newRavenRequest(r *http.Request) *raven.Request {
   203  	protocol := webutil.SchemeHTTP
   204  	if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
   205  		protocol = webutil.SchemeHTTPS
   206  	}
   207  	url := fmt.Sprintf("%s://%s%s", protocol, r.Host, r.URL.Path)
   208  	headers := make(map[string]string)
   209  	headers["Host"] = r.Host
   210  	var env map[string]string
   211  	if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil {
   212  		env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
   213  	}
   214  	return &raven.Request{
   215  		URL:         url,
   216  		Method:      r.Method,
   217  		QueryString: r.URL.RawQuery,
   218  		Headers:     headers,
   219  		Env:         env,
   220  	}
   221  }
   222  
   223  func errStackTrace(err error) *raven.Stacktrace {
   224  	if err != nil {
   225  		frames := errFrames(err)
   226  		if len(frames) > 0 {
   227  			return &raven.Stacktrace{
   228  				Frames: errFrames(err),
   229  			}
   230  		}
   231  		return nil
   232  	}
   233  	return nil
   234  }
   235  
   236  func errFrames(err error) []raven.Frame {
   237  	stacktrace := ex.ErrStackTrace(err)
   238  	if stacktrace == nil {
   239  		return nil
   240  	}
   241  	pointers, ok := stacktrace.(ex.StackPointers)
   242  	if !ok {
   243  		return nil
   244  	}
   245  
   246  	var output []raven.Frame
   247  	runtimeFrames := runtime.CallersFrames(pointers)
   248  	for {
   249  		callerFrame, more := runtimeFrames.Next()
   250  		// append in reverse order ...
   251  		output = append([]raven.Frame{
   252  			raven.NewFrame(callerFrame),
   253  		}, output...)
   254  		if !more {
   255  			break
   256  		}
   257  	}
   258  	return output
   259  }