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 }