github.com/goldeneggg/goa@v1.3.1/middleware/xray/segment.go (about)

     1  package xray
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/goadesign/goa"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  type (
    19  	// Segment represents a AWS X-Ray segment document.
    20  	Segment struct {
    21  		// Mutex used to synchronize access to segment.
    22  		*sync.Mutex
    23  		// Name is the name of the service reported to X-Ray.
    24  		Name string `json:"name"`
    25  		// Namespace identifies the source that created the segment.
    26  		Namespace string `json:"namespace"`
    27  		// Type is either the empty string or "subsegment".
    28  		Type string `json:"type,omitempty"`
    29  		// ID is a unique ID for the segment.
    30  		ID string `json:"id"`
    31  		// TraceID is the ID of the root trace.
    32  		TraceID string `json:"trace_id,omitempty"`
    33  		// ParentID is the ID of the parent segment when it is from a
    34  		// remote service. It is only initialized for the root segment.
    35  		ParentID string `json:"parent_id,omitempty"`
    36  		// StartTime is the segment start time.
    37  		StartTime float64 `json:"start_time,omitempty"`
    38  		// EndTime is the segment end time.
    39  		EndTime float64 `json:"end_time,omitempty"`
    40  		// InProgress is true if the segment hasn't completed yet.
    41  		InProgress bool `json:"in_progress"`
    42  		// HTTP contains the HTTP request and response information and is
    43  		// only initialized for the root segment.
    44  		HTTP *HTTP `json:"http,omitempty"`
    45  		// Cause contains information about an error that occurred while
    46  		// processing the request.
    47  		Cause *Cause `json:"cause,omitempty"`
    48  		// Error is true when a request causes an internal error. It is
    49  		// automatically set by Close when the response status code is
    50  		// 500 or more.
    51  		Error bool `json:"error"`
    52  		// Fault is true when a request results in an error that is due
    53  		// to the user. Typically it should be set when the response
    54  		// status code is between 400 and 500 (but not 429).
    55  		Fault bool `json:"fault"`
    56  		// Throttle is true when a request is throttled. It is set to
    57  		// true when the segment closes and the response status code is
    58  		// 429. Client code may set it to true manually as well.
    59  		Throttle bool `json:"throttle"`
    60  		// Annotations contains the segment annotations.
    61  		Annotations map[string]interface{} `json:"annotations,omitempty"`
    62  		// Metadata contains the segment metadata.
    63  		Metadata map[string]map[string]interface{} `json:"metadata,omitempty"`
    64  		// Subsegments contains all the subsegments.
    65  		Subsegments []*Segment `json:"subsegments,omitempty"`
    66  		// Parent is the subsegment parent, it's nil for the root
    67  		// segment.
    68  		Parent *Segment `json:"-"`
    69  		// conn is the UDP client to the X-Ray daemon.
    70  		conn net.Conn
    71  		// counter keeps track of the number of subsegments that have not
    72  		// completed yet.
    73  		counter int
    74  	}
    75  
    76  	// HTTP describes a HTTP request.
    77  	HTTP struct {
    78  		// Request contains the data reported about the incoming request.
    79  		Request *Request `json:"request,omitempty"`
    80  		// Response contains the data reported about the HTTP response.
    81  		Response *Response `json:"response,omitempty"`
    82  	}
    83  
    84  	// Request describes a HTTP request.
    85  	Request struct {
    86  		Method        string `json:"method,omitempty"`
    87  		URL           string `json:"url,omitempty"`
    88  		UserAgent     string `json:"user_agent,omitempty"`
    89  		ClientIP      string `json:"client_ip,omitempty"`
    90  		ContentLength int64  `json:"content_length"`
    91  	}
    92  
    93  	// Response describes a HTTP response.
    94  	Response struct {
    95  		Status        int   `json:"status"`
    96  		ContentLength int64 `json:"content_length"`
    97  	}
    98  
    99  	// Cause list errors that happens during the request.
   100  	Cause struct {
   101  		// ID to segment where error originated, exclusive with other
   102  		// fields.
   103  		ID string `json:"id,omitempty"`
   104  		// WorkingDirectory when error occurred. Exclusive with ID.
   105  		WorkingDirectory string `json:"working_directory,omitempty"`
   106  		// Exceptions contains the details on the error(s) that occurred
   107  		// when the request as processing.
   108  		Exceptions []*Exception `json:"exceptions,omitempty"`
   109  	}
   110  
   111  	// Exception describes an error.
   112  	Exception struct {
   113  		// Message contains the error message.
   114  		Message string `json:"message"`
   115  		// Stack is the error stack trace as initialized via the
   116  		// github.com/pkg/errors package.
   117  		Stack []*StackEntry `json:"stack"`
   118  	}
   119  
   120  	// StackEntry represents an entry in a error stacktrace.
   121  	StackEntry struct {
   122  		// Path to code file
   123  		Path string `json:"path"`
   124  		// Line number
   125  		Line int `json:"line"`
   126  		// Label is the line label if any
   127  		Label string `json:"label,omitempty"`
   128  	}
   129  
   130  	// key is the type used for context keys.
   131  	key int
   132  )
   133  
   134  const (
   135  	// udpHeader is the header of each segment sent to the daemon.
   136  	udpHeader = "{\"format\": \"json\", \"version\": 1}\n"
   137  
   138  	// maxStackDepth is the maximum number of stack frames reported.
   139  	maxStackDepth = 100
   140  )
   141  
   142  type (
   143  	causer interface {
   144  		Cause() error
   145  	}
   146  	stackTracer interface {
   147  		StackTrace() errors.StackTrace
   148  	}
   149  )
   150  
   151  // NewSegment creates a new segment that gets written to the given connection
   152  // on close.
   153  func NewSegment(name, traceID, spanID string, conn net.Conn) *Segment {
   154  	return &Segment{
   155  		Mutex:      &sync.Mutex{},
   156  		Name:       name,
   157  		TraceID:    traceID,
   158  		ID:         spanID,
   159  		StartTime:  now(),
   160  		InProgress: true,
   161  		conn:       conn,
   162  	}
   163  }
   164  
   165  // RecordRequest traces a request.
   166  //
   167  // It sets Http.Request & Namespace (ex: "remote")
   168  func (s *Segment) RecordRequest(req *http.Request, namespace string) {
   169  	s.Lock()
   170  	defer s.Unlock()
   171  
   172  	if s.HTTP == nil {
   173  		s.HTTP = &HTTP{}
   174  	}
   175  
   176  	s.Namespace = namespace
   177  	s.HTTP.Request = requestData(req)
   178  }
   179  
   180  // RecordResponse traces a response.
   181  //
   182  // It sets Throttle, Fault, Error and HTTP.Response
   183  func (s *Segment) RecordResponse(resp *http.Response) {
   184  	s.Lock()
   185  	defer s.Unlock()
   186  
   187  	if s.HTTP == nil {
   188  		s.HTTP = &HTTP{}
   189  	}
   190  
   191  	s.recordStatusCode(resp.StatusCode)
   192  
   193  	s.HTTP.Response = responseData(resp)
   194  }
   195  
   196  // RecordContextResponse traces a context response if present in the context
   197  //
   198  // It sets Throttle, Fault, Error and HTTP.Response
   199  func (s *Segment) RecordContextResponse(ctx context.Context) {
   200  	resp := goa.ContextResponse(ctx)
   201  	if resp == nil {
   202  		return
   203  	}
   204  
   205  	s.Lock()
   206  	defer s.Unlock()
   207  
   208  	if s.HTTP == nil {
   209  		s.HTTP = &HTTP{}
   210  	}
   211  
   212  	s.recordStatusCode(resp.Status)
   213  	s.HTTP.Response = &Response{resp.Status, int64(resp.Length)}
   214  }
   215  
   216  // RecordError traces an error. The client may also want to initialize the
   217  // fault field of s.
   218  //
   219  // The trace contains a stack trace and a cause for the error if the argument
   220  // was created using one of the New, Errorf, Wrap or Wrapf functions of the
   221  // github.com/pkg/errors package. Otherwise the Stack and Cause fields are empty.
   222  func (s *Segment) RecordError(e error) {
   223  	xerr := exceptionData(e)
   224  
   225  	s.Lock()
   226  	defer s.Unlock()
   227  
   228  	// set Error to indicate an internal error due to service being unreachable, etc.
   229  	// otherwise if a response was received then the status will determine Error vs. Fault.
   230  	//
   231  	// first check if the other flags have already been set in case these methods are being
   232  	// called directly instead of using xray.WrapClient(), etc.
   233  	if !(s.Fault || s.Throttle) {
   234  		s.Error = true
   235  	}
   236  	if s.Cause == nil {
   237  		wd, _ := os.Getwd()
   238  		s.Cause = &Cause{WorkingDirectory: wd}
   239  	}
   240  	s.Cause.Exceptions = append(s.Cause.Exceptions, xerr)
   241  	p := s.Parent
   242  	for p != nil {
   243  		if p.Cause == nil {
   244  			p.Cause = &Cause{ID: s.ID}
   245  		}
   246  		p = p.Parent
   247  	}
   248  }
   249  
   250  // NewSubsegment creates a subsegment of s.
   251  func (s *Segment) NewSubsegment(name string) *Segment {
   252  	s.Lock()
   253  	defer s.Unlock()
   254  
   255  	sub := &Segment{
   256  		Mutex:      &sync.Mutex{},
   257  		ID:         NewID(),
   258  		TraceID:    s.TraceID,
   259  		ParentID:   s.ID,
   260  		Type:       "subsegment",
   261  		Name:       name,
   262  		StartTime:  now(),
   263  		InProgress: true,
   264  		Parent:     s,
   265  		conn:       s.conn,
   266  	}
   267  	s.Subsegments = append(s.Subsegments, sub)
   268  	s.counter++
   269  	return sub
   270  }
   271  
   272  // Capture creates a subsegment to record the execution of the given function.
   273  // Usage:
   274  //
   275  //     s := xray.ContextSegment(ctx)
   276  //     s.Capture("slow-func", func() {
   277  //         // ... some long executing code
   278  //     })
   279  //
   280  func (s *Segment) Capture(name string, fn func()) {
   281  	sub := s.NewSubsegment(name)
   282  	defer sub.Close()
   283  	fn()
   284  }
   285  
   286  // AddAnnotation adds a key-value pair that can be queried by AWS X-Ray.
   287  func (s *Segment) AddAnnotation(key string, value string) {
   288  	s.addAnnotation(key, value)
   289  }
   290  
   291  // AddInt64Annotation adds a key-value pair that can be queried by AWS X-Ray.
   292  func (s *Segment) AddInt64Annotation(key string, value int64) {
   293  	s.addAnnotation(key, value)
   294  }
   295  
   296  // AddBoolAnnotation adds a key-value pair that can be queried by AWS X-Ray.
   297  func (s *Segment) AddBoolAnnotation(key string, value bool) {
   298  	s.addAnnotation(key, value)
   299  }
   300  
   301  // addAnnotation adds a key-value pair that can be queried by AWS X-Ray.
   302  // AWS X-Ray only supports annotations of type string, integer or boolean.
   303  func (s *Segment) addAnnotation(key string, value interface{}) {
   304  	s.Lock()
   305  	defer s.Unlock()
   306  
   307  	if s.Annotations == nil {
   308  		s.Annotations = make(map[string]interface{})
   309  	}
   310  	s.Annotations[key] = value
   311  }
   312  
   313  // AddMetadata adds a key-value pair to the metadata.default attribute.
   314  // Metadata is not queryable, but is recorded.
   315  func (s *Segment) AddMetadata(key string, value string) {
   316  	s.addMetadata(key, value)
   317  }
   318  
   319  // AddInt64Metadata adds a key-value pair that can be queried by AWS X-Ray.
   320  func (s *Segment) AddInt64Metadata(key string, value int64) {
   321  	s.addMetadata(key, value)
   322  }
   323  
   324  // AddBoolMetadata adds a key-value pair that can be queried by AWS X-Ray.
   325  func (s *Segment) AddBoolMetadata(key string, value bool) {
   326  	s.addMetadata(key, value)
   327  }
   328  
   329  // addMetadata adds a key-value pair that can be queried by AWS X-Ray.
   330  // AWS X-Ray only supports annotations of type string, integer or boolean.
   331  func (s *Segment) addMetadata(key string, value interface{}) {
   332  	s.Lock()
   333  	defer s.Unlock()
   334  
   335  	if s.Metadata == nil {
   336  		s.Metadata = make(map[string]map[string]interface{})
   337  		s.Metadata["default"] = make(map[string]interface{})
   338  	}
   339  	s.Metadata["default"][key] = value
   340  }
   341  
   342  // Close closes the segment by setting its EndTime.
   343  func (s *Segment) Close() {
   344  	s.Lock()
   345  	defer s.Unlock()
   346  
   347  	s.EndTime = now()
   348  	s.InProgress = false
   349  	if s.Parent != nil {
   350  		s.Parent.decrementCounter()
   351  	}
   352  	if s.counter <= 0 {
   353  		s.flush()
   354  	}
   355  }
   356  
   357  // flush sends the segment to the AWS X-Ray daemon.
   358  func (s *Segment) flush() {
   359  	b, _ := json.Marshal(s)
   360  	// append so we make only one call to Write to be goroutine-safe
   361  	s.conn.Write(append([]byte(udpHeader), b...))
   362  }
   363  
   364  // recordStatusCode sets Throttle, Fault, Error
   365  //
   366  // It is expected that the mutex has already been locked when calling this method.
   367  func (s *Segment) recordStatusCode(statusCode int) {
   368  	switch {
   369  	case statusCode == http.StatusTooManyRequests:
   370  		s.Throttle = true
   371  	case statusCode >= 400 && statusCode < 500:
   372  		s.Fault = true
   373  	case statusCode >= 500:
   374  		s.Error = true
   375  	}
   376  }
   377  
   378  // decrementCounter decrements the segment counter and flushes it if it's 0.
   379  func (s *Segment) decrementCounter() {
   380  	s.Lock()
   381  	defer s.Unlock()
   382  
   383  	s.counter--
   384  	if s.counter <= 0 && s.EndTime != 0 {
   385  		// Segment is closed and last subsegment closed, flush it
   386  		s.flush()
   387  	}
   388  }
   389  
   390  // exceptionData creates an Exception from an error.
   391  func exceptionData(e error) *Exception {
   392  	var xerr *Exception
   393  	if c, ok := e.(causer); ok {
   394  		xerr = &Exception{Message: c.Cause().Error()}
   395  	} else {
   396  		xerr = &Exception{Message: e.Error()}
   397  	}
   398  	if s, ok := e.(stackTracer); ok {
   399  		st := s.StackTrace()
   400  		ln := len(st)
   401  		if ln > maxStackDepth {
   402  			ln = maxStackDepth
   403  		}
   404  		frames := make([]*StackEntry, ln)
   405  		for i := 0; i < ln; i++ {
   406  			f := st[i]
   407  			line, _ := strconv.Atoi(fmt.Sprintf("%d", f))
   408  			frames[i] = &StackEntry{
   409  				Path:  fmt.Sprintf("%s", f),
   410  				Line:  line,
   411  				Label: fmt.Sprintf("%n", f),
   412  			}
   413  		}
   414  		xerr.Stack = frames
   415  	}
   416  
   417  	return xerr
   418  }
   419  
   420  // requestData creates a Request from a http.Request.
   421  func requestData(req *http.Request) *Request {
   422  	var (
   423  		scheme = "http"
   424  		host   = req.Host
   425  	)
   426  	if len(req.URL.Scheme) > 0 {
   427  		scheme = req.URL.Scheme
   428  	}
   429  	if len(req.URL.Host) > 0 {
   430  		host = req.URL.Host
   431  	}
   432  
   433  	return &Request{
   434  		Method:        req.Method,
   435  		URL:           fmt.Sprintf("%s://%s%s", scheme, host, req.URL.Path),
   436  		ClientIP:      getIP(req),
   437  		UserAgent:     req.UserAgent(),
   438  		ContentLength: req.ContentLength,
   439  	}
   440  }
   441  
   442  // responseData creates a Response from a http.Response.
   443  func responseData(resp *http.Response) *Response {
   444  	return &Response{
   445  		Status:        resp.StatusCode,
   446  		ContentLength: resp.ContentLength,
   447  	}
   448  }
   449  
   450  // getIP implements a heuristic that returns an origin IP address for a request.
   451  func getIP(req *http.Request) string {
   452  	for _, h := range []string{"X-Forwarded-For", "X-Real-Ip"} {
   453  		for _, ip := range strings.Split(req.Header.Get(h), ",") {
   454  			if len(ip) == 0 {
   455  				continue
   456  			}
   457  			realIP := net.ParseIP(strings.Replace(ip, " ", "", -1))
   458  			return realIP.String()
   459  		}
   460  	}
   461  
   462  	// not found in header
   463  	host, _, err := net.SplitHostPort(req.RemoteAddr)
   464  	if err != nil {
   465  		return req.RemoteAddr
   466  	}
   467  	return host
   468  }