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