github.com/ManabuSeki/goa-v1@v1.4.3/middleware/xray/middleware.go (about)

     1  package xray
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/goadesign/goa"
    13  	"github.com/goadesign/goa/middleware"
    14  )
    15  
    16  const (
    17  	// segKey is the key used to store the segments in the context.
    18  	segKey key = iota + 1
    19  )
    20  
    21  // New returns a middleware that sends AWS X-Ray segments to the daemon running
    22  // at the given address.
    23  //
    24  // service is the name of the service reported to X-Ray. daemon is the hostname
    25  // (including port) of the X-Ray daemon collecting the segments.
    26  //
    27  // The middleware works by extracting the trace information from the context
    28  // using the tracing middleware package. The tracing middleware must be mounted
    29  // first on the service.
    30  //
    31  // The middleware stores the request segment in the context. Use ContextSegment
    32  // to retrieve it. User code can further configure the segment for example to set
    33  // a service version or record an error.
    34  //
    35  // User code may create child segments using the Segment NewSubsegment method
    36  // for tracing requests to external services. Such segments should be closed via
    37  // the Close method once the request completes. The middleware takes care of
    38  // closing the top level segment. Typical usage:
    39  //
    40  //     segment := xray.ContextSegment(ctx)
    41  //     sub := segment.NewSubsegment("external-service")
    42  //     defer sub.Close()
    43  //     err := client.MakeRequest()
    44  //     if err != nil {
    45  //         sub.Error = xray.Wrap(err)
    46  //     }
    47  //     return
    48  //
    49  // An X-Ray trace is limited to 500 KB of segment data (JSON) being submitted
    50  // for it. See: https://aws.amazon.com/xray/pricing/
    51  //
    52  // Traces running for multiple minutes may encounter additional dynamic limits,
    53  // resulting in the trace being limited to less than 500 KB. The workaround is
    54  // to send less data -- fewer segments, subsegments, annotations, or metadata.
    55  // And perhaps split up a single large trace into several different traces.
    56  //
    57  // Here are some observations of the relationship between trace duration and
    58  // the number of bytes that could be sent successfully:
    59  //   - 49 seconds: 543 KB
    60  //   - 2.4 minutes: 51 KB
    61  //   - 6.8 minutes: 14 KB
    62  //   - 1.4 hours:   14 KB
    63  //
    64  // Besides those varying size limitations, a trace may be open for up to 7 days.
    65  func New(service, daemon string) (goa.Middleware, error) {
    66  	connection, err := periodicallyRedialingConn(context.Background(), time.Minute, func() (net.Conn, error) {
    67  		return net.Dial("udp", daemon)
    68  	})
    69  	if err != nil {
    70  		return nil, fmt.Errorf("xray: failed to connect to daemon - %s", err)
    71  	}
    72  	return func(h goa.Handler) goa.Handler {
    73  		return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
    74  			var (
    75  				err     error
    76  				traceID = middleware.ContextTraceID(ctx)
    77  			)
    78  			if traceID == "" {
    79  				// No tracing
    80  				return h(ctx, rw, req)
    81  			}
    82  
    83  			s := newSegment(ctx, traceID, service, req, connection())
    84  			ctx = WithSegment(ctx, s)
    85  
    86  			defer func() {
    87  				go func() {
    88  					defer s.Close()
    89  
    90  					s.RecordContextResponse(ctx)
    91  					if err != nil {
    92  						s.RecordError(err)
    93  					}
    94  				}()
    95  			}()
    96  
    97  			err = h(ctx, rw, req)
    98  
    99  			return err
   100  		}
   101  	}, nil
   102  }
   103  
   104  // NewID is a span ID creation algorithm which produces values that are
   105  // compatible with AWS X-Ray.
   106  func NewID() string {
   107  	b := make([]byte, 8)
   108  	rand.Read(b)
   109  	return fmt.Sprintf("%x", b)
   110  }
   111  
   112  // NewTraceID is a trace ID creation algorithm which produces values that are
   113  // compatible with AWS X-Ray.
   114  func NewTraceID() string {
   115  	b := make([]byte, 12)
   116  	rand.Read(b)
   117  	return fmt.Sprintf("%d-%x-%s", 1, time.Now().Unix(), fmt.Sprintf("%x", b))
   118  }
   119  
   120  // WithSegment creates a context containing the given segment. Use ContextSegment
   121  // to retrieve it.
   122  func WithSegment(ctx context.Context, s *Segment) context.Context {
   123  	return context.WithValue(ctx, segKey, s)
   124  }
   125  
   126  // ContextSegment extracts the segment set in the context with WithSegment.
   127  func ContextSegment(ctx context.Context) *Segment {
   128  	if s := ctx.Value(segKey); s != nil {
   129  		return s.(*Segment)
   130  	}
   131  	return nil
   132  }
   133  
   134  // newSegment creates a new segment for the incoming request.
   135  func newSegment(ctx context.Context, traceID, name string, req *http.Request, c net.Conn) *Segment {
   136  	var (
   137  		spanID   = middleware.ContextSpanID(ctx)
   138  		parentID = middleware.ContextParentSpanID(ctx)
   139  	)
   140  
   141  	s := NewSegment(name, traceID, spanID, c)
   142  	s.RecordRequest(req, "")
   143  	if parentID != "" {
   144  		s.ParentID = parentID
   145  	}
   146  	s.SubmitInProgress()
   147  
   148  	return s
   149  }
   150  
   151  // now returns the current time as a float appropriate for X-Ray processing.
   152  func now() float64 {
   153  	return float64(time.Now().Truncate(time.Millisecond).UnixNano()) / 1e9
   154  }
   155  
   156  // periodicallyRedialingConn creates a goroutine to periodically re-dial a connection, so the hostname can be
   157  // re-resolved if the IP changes.
   158  // Returns a func that provides the latest Conn value.
   159  func periodicallyRedialingConn(ctx context.Context, renewPeriod time.Duration, dial func() (net.Conn, error)) (func() net.Conn, error) {
   160  	var (
   161  		err error
   162  
   163  		// guard access to c
   164  		mu sync.RWMutex
   165  		c  net.Conn
   166  	)
   167  
   168  	// get an initial connection
   169  	if c, err = dial(); err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	// periodically re-dial
   174  	go func() {
   175  		ticker := time.NewTicker(renewPeriod)
   176  		for {
   177  			select {
   178  			case <-ticker.C:
   179  				newConn, err := dial()
   180  				if err != nil {
   181  					continue // we don't have anything better to replace `c` with
   182  				}
   183  				mu.Lock()
   184  				c = newConn
   185  				mu.Unlock()
   186  			case <-ctx.Done():
   187  				return
   188  			}
   189  		}
   190  	}()
   191  
   192  	return func() net.Conn {
   193  		mu.RLock()
   194  		defer mu.RUnlock()
   195  		return c
   196  	}, nil
   197  }