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 }