github.com/brycereitano/goa@v0.0.0-20170315073847-8ffa6c85e265/middleware/xray/segment.go (about)

     1  package xray
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net"
     7  	"os"
     8  	"strconv"
     9  	"sync"
    10  
    11  	"github.com/pkg/errors"
    12  )
    13  
    14  type (
    15  	// Segment represents a AWS X-Ray segment document.
    16  	Segment struct {
    17  		// Mutex used to synchronize access to segment.
    18  		*sync.Mutex
    19  		// Name is the name of the service reported to X-Ray.
    20  		Name string `json:"name"`
    21  		// Namespace identifies the source that created the segment.
    22  		Namespace string `json:"namespace"`
    23  		// Type is either the empty string or "subsegment".
    24  		Type string `json:"type,omitempty"`
    25  		// ID is a unique ID for the segment.
    26  		ID string `json:"id"`
    27  		// TraceID is the ID of the root trace.
    28  		TraceID string `json:"trace_id,omitempty"`
    29  		// ParentID is the ID of the parent segment when it is from a
    30  		// remote service. It is only initialized for the root segment.
    31  		ParentID string `json:"parent_id,omitempty"`
    32  		// StartTime is the segment start time.
    33  		StartTime float64 `json:"start_time,omitempty"`
    34  		// EndTime is the segment end time.
    35  		EndTime float64 `json:"end_time,omitempty"`
    36  		// InProgress is true if the segment hasn't completed yet.
    37  		InProgress bool `json:"in_progress"`
    38  		// HTTP contains the HTTP request and response information and is
    39  		// only initialized for the root segment.
    40  		HTTP *HTTP `json:"http,omitempty"`
    41  		// Cause contains information about an error that occurred while
    42  		// processing the request.
    43  		Cause *Cause `json:"cause,omitempty"`
    44  		// Error is true when a request causes an internal error. It is
    45  		// automatically set by Close when the response status code is
    46  		// 500 or more.
    47  		Error bool `json:"error"`
    48  		// Fault is true when a request results in an error that is due
    49  		// to the user. Typically it should be set when the response
    50  		// status code is between 400 and 500 (but not 429).
    51  		Fault bool `json:"fault"`
    52  		// Throttle is true when a request is throttled. It is set to
    53  		// true when the segment closes and the response status code is
    54  		// 429. Client code may set it to true manually as well.
    55  		Throttle bool `json:"throttle"`
    56  		// Annotations contains the segment annotations.
    57  		Annotations map[string]interface{} `json:"annotations,omitempty"`
    58  		// Metadata contains the segment metadata.
    59  		Metadata map[string]map[string]interface{} `json:"metadata,omitempty"`
    60  		// Subsegments contains all the subsegments.
    61  		Subsegments []*Segment `json:"subsegments,omitempty"`
    62  		// Parent is the subsegment parent, it's nil for the root
    63  		// segment.
    64  		Parent *Segment `json:"-"`
    65  		// conn is the UDP client to the X-Ray daemon.
    66  		conn net.Conn
    67  		// counter keeps track of the number of subsegments that have not
    68  		// completed yet.
    69  		counter int
    70  	}
    71  
    72  	// HTTP describes a HTTP request.
    73  	HTTP struct {
    74  		// Request contains the data reported about the incoming request.
    75  		Request *Request `json:"request,omitempty"`
    76  		// Response contains the data reported about the HTTP response.
    77  		Response *Response `json:"response,omitempty"`
    78  	}
    79  
    80  	// Request describes a HTTP request.
    81  	Request struct {
    82  		Method    string `json:"method,omitempty"`
    83  		URL       string `json:"url,omitempty"`
    84  		UserAgent string `json:"user_agent,omitempty"`
    85  		ClientIP  string `json:"client_ip,omitempty"`
    86  	}
    87  
    88  	// Response describes a HTTP response.
    89  	Response struct {
    90  		Status        int `json:"status"`
    91  		ContentLength int `json:"content_length"`
    92  	}
    93  
    94  	// Cause list errors that happens during the request.
    95  	Cause struct {
    96  		// ID to segment where error originated, exclusive with other
    97  		// fields.
    98  		ID string `json:"id,omitempty"`
    99  		// WorkingDirectory when error occurred. Exclusive with ID.
   100  		WorkingDirectory string `json:"working_directory,omitempty"`
   101  		// Exceptions contains the details on the error(s) that occurred
   102  		// when the request as processing.
   103  		Exceptions []*Exception `json:"exceptions,omitempty"`
   104  	}
   105  
   106  	// Exception describes an error.
   107  	Exception struct {
   108  		// Message contains the error message.
   109  		Message string `json:"message"`
   110  		// Stack is the error stack trace as initialized via the
   111  		// github.com/pkg/errors package.
   112  		Stack []*StackEntry `json:"stack"`
   113  	}
   114  
   115  	// StackEntry represents an entry in a error stacktrace.
   116  	StackEntry struct {
   117  		// Path to code file
   118  		Path string `json:"path"`
   119  		// Line number
   120  		Line int `json:"line"`
   121  		// Label is the line label if any
   122  		Label string `json:"label,omitempty"`
   123  	}
   124  
   125  	// key is the type used for context keys.
   126  	key int
   127  )
   128  
   129  const (
   130  	// udpHeader is the header of each segment sent to the daemon.
   131  	udpHeader = "{\"format\": \"json\", \"version\": 1}\n"
   132  
   133  	// maxStackDepth is the maximum number of stack frames reported.
   134  	maxStackDepth = 100
   135  )
   136  
   137  type (
   138  	causer interface {
   139  		Cause() error
   140  	}
   141  	stackTracer interface {
   142  		StackTrace() errors.StackTrace
   143  	}
   144  )
   145  
   146  // NewSegment creates a new segment that gets written to the given connection
   147  // on close.
   148  func NewSegment(name, traceID, spanID string, conn net.Conn) *Segment {
   149  	return &Segment{
   150  		Mutex:      &sync.Mutex{},
   151  		Name:       name,
   152  		TraceID:    traceID,
   153  		ID:         spanID,
   154  		StartTime:  now(),
   155  		InProgress: true,
   156  		conn:       conn,
   157  	}
   158  }
   159  
   160  // RecordError traces an error. The client may also want to initialize the
   161  // fault field of s.
   162  //
   163  // The trace contains a stack trace and a cause for the error if the argument
   164  // was created using one of the New, Errorf, Wrap or Wrapf functions of the
   165  // github.com/pkg/errors package. Otherwise the Stack and Cause fields are empty.
   166  func (s *Segment) RecordError(e error) {
   167  	var xerr *Exception
   168  	if c, ok := e.(causer); ok {
   169  		xerr = &Exception{Message: c.Cause().Error()}
   170  	} else {
   171  		xerr = &Exception{Message: e.Error()}
   172  	}
   173  	if s, ok := e.(stackTracer); ok {
   174  		st := s.StackTrace()
   175  		ln := len(st)
   176  		if ln > maxStackDepth {
   177  			ln = maxStackDepth
   178  		}
   179  		frames := make([]*StackEntry, ln)
   180  		for i := 0; i < ln; i++ {
   181  			f := st[i]
   182  			line, _ := strconv.Atoi(fmt.Sprintf("%d", f))
   183  			frames[i] = &StackEntry{
   184  				Path:  fmt.Sprintf("%s", f),
   185  				Line:  line,
   186  				Label: fmt.Sprintf("%n", f),
   187  			}
   188  		}
   189  		xerr.Stack = frames
   190  	}
   191  
   192  	s.Lock()
   193  	defer s.Unlock()
   194  	if s.Cause == nil {
   195  		wd, _ := os.Getwd()
   196  		s.Cause = &Cause{WorkingDirectory: wd}
   197  	}
   198  	s.Cause.Exceptions = append(s.Cause.Exceptions, xerr)
   199  	p := s.Parent
   200  	for p != nil {
   201  		if p.Cause == nil {
   202  			p.Cause = &Cause{ID: s.ID}
   203  		}
   204  		p = p.Parent
   205  	}
   206  }
   207  
   208  // NewSubsegment creates a subsegment of s.
   209  func (s *Segment) NewSubsegment(name string) *Segment {
   210  	s.Lock()
   211  	defer s.Unlock()
   212  	sub := &Segment{
   213  		Mutex:      &sync.Mutex{},
   214  		ID:         NewID(),
   215  		TraceID:    s.TraceID,
   216  		ParentID:   s.ID,
   217  		Type:       "subsegment",
   218  		Name:       name,
   219  		StartTime:  now(),
   220  		InProgress: true,
   221  		Parent:     s,
   222  		conn:       s.conn,
   223  	}
   224  	s.Subsegments = append(s.Subsegments, sub)
   225  	s.counter++
   226  	return sub
   227  }
   228  
   229  // Capture creates a subsegment to record the execution of the given function.
   230  // Usage:
   231  //
   232  //     s := xray.ContextSegment(ctx)
   233  //     s.Capture("slow-func", func() {
   234  //         // ... some long executing code
   235  //     })
   236  //
   237  func (s *Segment) Capture(name string, fn func()) {
   238  	sub := s.NewSubsegment(name)
   239  	defer sub.Close()
   240  	fn()
   241  }
   242  
   243  // AddAnnotation adds a key-value pair that can be queried by AWS X-Ray.
   244  func (s *Segment) AddAnnotation(key string, value string) {
   245  	s.addAnnotation(key, value)
   246  }
   247  
   248  // AddInt64Annotation adds a key-value pair that can be queried by AWS X-Ray.
   249  func (s *Segment) AddInt64Annotation(key string, value int64) {
   250  	s.addAnnotation(key, value)
   251  }
   252  
   253  // AddBoolAnnotation adds a key-value pair that can be queried by AWS X-Ray.
   254  func (s *Segment) AddBoolAnnotation(key string, value bool) {
   255  	s.addAnnotation(key, value)
   256  }
   257  
   258  // addAnnotation adds a key-value pair that can be queried by AWS X-Ray.
   259  // AWS X-Ray only supports annotations of type string, integer or boolean.
   260  func (s *Segment) addAnnotation(key string, value interface{}) {
   261  	s.Lock()
   262  	defer s.Unlock()
   263  	if s.Annotations == nil {
   264  		s.Annotations = make(map[string]interface{})
   265  	}
   266  	s.Annotations[key] = value
   267  }
   268  
   269  // AddMetadata adds a key-value pair to the metadata.default attribute.
   270  // Metadata is not queryable, but is recorded.
   271  func (s *Segment) AddMetadata(key string, value string) {
   272  	s.addMetadata(key, value)
   273  }
   274  
   275  // AddInt64Metadata adds a key-value pair that can be queried by AWS X-Ray.
   276  func (s *Segment) AddInt64Metadata(key string, value int64) {
   277  	s.addMetadata(key, value)
   278  }
   279  
   280  // AddBoolMetadata adds a key-value pair that can be queried by AWS X-Ray.
   281  func (s *Segment) AddBoolMetadata(key string, value bool) {
   282  	s.addMetadata(key, value)
   283  }
   284  
   285  // addMetadata adds a key-value pair that can be queried by AWS X-Ray.
   286  // AWS X-Ray only supports annotations of type string, integer or boolean.
   287  func (s *Segment) addMetadata(key string, value interface{}) {
   288  	s.Lock()
   289  	defer s.Unlock()
   290  	if s.Metadata == nil {
   291  		s.Metadata = make(map[string]map[string]interface{})
   292  		s.Metadata["default"] = make(map[string]interface{})
   293  	}
   294  	s.Metadata["default"][key] = value
   295  }
   296  
   297  // Close closes the segment by setting its EndTime.
   298  func (s *Segment) Close() {
   299  	s.Lock()
   300  	defer s.Unlock()
   301  	s.EndTime = now()
   302  	s.InProgress = false
   303  	if s.Parent != nil {
   304  		s.Parent.decrementCounter()
   305  	}
   306  	if s.counter <= 0 {
   307  		s.flush()
   308  	}
   309  }
   310  
   311  // flush sends the segment to the AWS X-Ray daemon.
   312  func (s *Segment) flush() {
   313  	b, _ := json.Marshal(s)
   314  	// append so we make only one call to Write to be goroutine-safe
   315  	s.conn.Write(append([]byte(udpHeader), b...))
   316  }
   317  
   318  // decrementCounter decrements the segment counter and flushes it if it's 0.
   319  func (s *Segment) decrementCounter() {
   320  	s.Lock()
   321  	defer s.Unlock()
   322  	s.counter--
   323  	if s.counter <= 0 && s.EndTime != 0 {
   324  		// Segment is closed and last subsegment closed, flush it
   325  		s.flush()
   326  	}
   327  }