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 }