github.com/aldelo/common@v1.5.1/wrapper/xray/xray.go (about) 1 package xray 2 3 /* 4 * Copyright 2020-2023 Aldelo, LP 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 // ================================================================================================================= 20 // AWS CREDENTIAL: 21 // use $> aws configure (to set aws access key and secret to target machine) 22 // Store AWS Access ID and Secret Key into Default Profile Using '$ aws configure' cli 23 // 24 // To Install & Setup AWS CLI on Host: 25 // 1) https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html 26 // On Ubuntu, if host does not have zip and unzip: 27 // $> sudo apt install zip 28 // $> sudo apt install unzip 29 // On Ubuntu, to install AWS CLI v2: 30 // $> curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 31 // $> unzip awscliv2.zip 32 // $> sudo ./aws/install 33 // 2) $> aws configure set region awsRegionName --profile default 34 // 3) $> aws configure 35 // follow prompts to enter Access ID and Secret Key 36 // 37 // AWS Region Name Reference: 38 // us-west-2, us-east-1, ap-northeast-1, etc 39 // See: https://docs.aws.amazon.com/general/latest/gr/rande.html 40 // ================================================================================================================= 41 42 import ( 43 "context" 44 "fmt" 45 util "github.com/aldelo/common" 46 "github.com/aws/aws-xray-sdk-go/awsplugins/ecs" 47 "github.com/aws/aws-xray-sdk-go/header" 48 "github.com/aws/aws-xray-sdk-go/xray" 49 "net/http" 50 "os" 51 "sync" 52 ) 53 54 // 55 // aws xray helpers provides some wrapper functions for aws xray service access functionality 56 // 57 // aws xray deploy to EC2 (Linux) requires xray daemon, see deploy info below: 58 // https://docs.aws.amazon.com/xray/latest/devguide/xray-daemon.html 59 // 60 // aws ec2 / ecs xray daemon install: 61 // EC2 (Linux) 62 // #!/bin/bash 63 // curl https://s3.dualstack.us-east-1.amazonaws.com/aws-xray-assets.us-east-1/xray-daemon/aws-xray-daemon-3.x.deb -o /home/ubuntu/xray.deb 64 // sudo apt install /home/ubuntu/xray.deb 65 // ECS 66 // Create a folder and download the daemon 67 // ~$ mkdir xray-daemon && cd xray-daemon 68 // ~/xray-daemon$ curl https://s3.dualstack.us-east-1.amazonaws.com/aws-xray-assets.us-east-1/xray-daemon/aws-xray-daemon-linux-3.x.zip -o ./aws-xray-daemon-linux-3.x.zip 69 // ~/xray-daemon$ unzip -o aws-xray-daemon-linux-3.x.zip -d . 70 // Create a Dockerfile with the following content 71 // *~/xray-daemon/Dockerfile* 72 // FROM ubuntu:12.04 73 // COPY xray /usr/bin/xray-daemon 74 // CMD xray-daemon -f /var/log/xray-daemon.log & 75 // Build the image 76 // ~/xray-daemon$ docker build -t xray . 77 // 78 // aws xray daemon service launch 79 // On ubuntu, after apt install, daemon service starts automatically 80 // 81 // aws xray Security Setup 82 // IAM-Role-EC2 (The Role Starting EC2) 83 // Requires “xray:PutTraceSegments, etc.” Security 84 // 85 // aws xray concepts documentation: 86 // https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html 87 88 type XRayParentSegment struct { 89 SegmentID string 90 TraceID string 91 } 92 93 // ================================================================================================================ 94 // aws xray helper functions 95 // ================================================================================================================ 96 97 // indicates if xray service tracing is on or off 98 var _xrayServiceOn bool 99 var _mu sync.RWMutex 100 101 // Init will configure xray daemon address and service version 102 func Init(daemonAddr string, serviceVersion string) error { 103 104 // conditionally load plugin 105 if os.Getenv("ENVIRONMENT") == "ECS" { 106 ecs.Init() 107 } 108 109 if util.LenTrim(daemonAddr) == 0 { 110 // if daemon address is not set, 111 // use default value 112 daemonAddr = "127.0.0.1:2000" 113 } 114 115 if util.LenTrim(serviceVersion) == 0 { 116 // if service version is not set, 117 // use default value 118 serviceVersion = "1.2.0" 119 } 120 121 return xray.Configure(xray.Config{ 122 DaemonAddr: daemonAddr, 123 ServiceVersion: serviceVersion, 124 }) 125 } 126 127 // SetXRayServiceOn turns on xray service for new objects, 128 // so that wrappers and code supporting xray will start using xray for tracing, 129 // the service is set to on during its init, open, or connect etc actions, 130 // existing objects are not affected by this function action 131 func SetXRayServiceOn() { 132 _mu.Lock() 133 defer _mu.Unlock() 134 _xrayServiceOn = true 135 } 136 137 // SetXRayServiceOff turns off xray service for new objects, 138 // so that wrappers and code supporting xray will not start using xray for tracing when it is init, connect or open, 139 // existing objects are not affected by this function action 140 func SetXRayServiceOff() { 141 _mu.Lock() 142 defer _mu.Unlock() 143 _xrayServiceOn = false 144 } 145 146 // XRayServiceOn returns whether xray tracing service is on or off 147 func XRayServiceOn() bool { 148 _mu.RLock() 149 defer _mu.RUnlock() 150 return _xrayServiceOn 151 } 152 153 // DisableTracing disables xray tracing 154 func DisableTracing() { 155 _ = os.Setenv("AWS_XRAY_SDK_DISABLED", "TRUE") 156 } 157 158 // EnableTracing re-enables xray tracing 159 func EnableTracing() { 160 _ = os.Setenv("AWS_XRAY_SDK_DISABLED", "FALSE") 161 } 162 163 // GetXRayHeader gets header from http.request, 164 // if headerName is not specified, defaults to x-amzn-trace-id 165 func GetXRayHeader(req *http.Request, headerName ...string) *header.Header { 166 if req == nil { 167 return nil 168 } else if req.Header == nil { 169 return nil 170 } 171 172 headerKey := "X-Amzn-Trace-Id" 173 174 if len(headerName) > 0 && util.LenTrim(headerName[0]) > 0 { 175 headerKey = headerName[0] 176 } 177 178 return header.FromString(req.Header.Get(headerKey)) 179 } 180 181 // GetXRayHttpRequestName returns the segment name based on http request method and path, 182 // or if segmentNamer is defined, gets the name from http request host 183 func GetXRayHttpRequestName(req *http.Request, segNamer ...xray.SegmentNamer) string { 184 if req == nil { 185 return "" 186 } 187 188 if len(segNamer) > 0 && segNamer[0] != nil { 189 return segNamer[0].Name(req.Host) 190 } 191 192 if req.URL != nil { 193 return fmt.Sprintf("%s %s%s", req.Method, req.Host, req.URL.Path) 194 } else { 195 return fmt.Sprintf("%s %s", req.Method, req.Host) 196 } 197 } 198 199 // ================================================================================================================ 200 // aws xray helper struct 201 // ================================================================================================================ 202 203 // XSegment struct provides wrapper function for xray segment, subsegment, context, and related actions 204 // 205 // always use NewSegment() to create XSegment object ptr 206 type XSegment struct { 207 Ctx context.Context 208 Seg *xray.Segment 209 210 // indicates if this segment is ready for use 211 _segReady bool 212 } 213 214 // XTraceData contains maps of data to add during trace activity 215 type XTraceData struct { 216 Meta map[string]interface{} 217 Annotations map[string]interface{} 218 Errors map[string]error 219 } 220 221 // AddMeta adds xray metadata to trace, 222 // metadata = key value pair with value of any type, including objects or lists, not indexed, 223 // 224 // used to record data that need to be stored in the trace, but don't need to be indexed for searching 225 func (t *XTraceData) AddMeta(key string, data interface{}) { 226 if util.LenTrim(key) == 0 { 227 return 228 } 229 230 if data == nil { 231 return 232 } 233 234 if t.Meta == nil { 235 t.Meta = make(map[string]interface{}) 236 } 237 238 t.Meta[key] = data 239 } 240 241 // AddAnnotation adds xray annotation to trace, 242 // each trace limits to 50 annotations, 243 // annotation = key value pair indexed for use with filter expression 244 func (t *XTraceData) AddAnnotation(key string, data interface{}) { 245 if util.LenTrim(key) == 0 { 246 return 247 } 248 249 if data == nil { 250 return 251 } 252 253 if t.Annotations == nil { 254 t.Annotations = make(map[string]interface{}) 255 } 256 257 t.Annotations[key] = data 258 } 259 260 // AddError adds xray error to trace, 261 // Error = client errors (4xx other than 429) 262 // Fault = server faults (5xx) 263 // Throttle = throttle errors (429 too many requests) 264 func (t *XTraceData) AddError(key string, err error) { 265 if util.LenTrim(key) == 0 { 266 return 267 } 268 269 if err == nil { 270 return 271 } 272 273 if t.Errors == nil { 274 t.Errors = make(map[string]error) 275 } 276 277 t.Errors[key] = err 278 } 279 280 // NewSubSegmentFromContext begins a new subsegment under the parent segment context, 281 // context can not be empty, and must contains parent segment info 282 func NewSubSegmentFromContext(ctx context.Context, serviceNameOrUrl string) *XSegment { 283 if util.LenTrim(serviceNameOrUrl) == 0 { 284 serviceNameOrUrl = "no.service.name.defined" 285 } 286 subCtx, seg := xray.BeginSubsegment(ctx, serviceNameOrUrl) 287 288 return &XSegment{ 289 Ctx: subCtx, 290 Seg: seg, 291 _segReady: true, 292 } 293 } 294 295 // NewSegment begins a new segment for a named service or url, 296 // the context.Background() is used as the base context when creating a new segment 297 // 298 // NOTE = ALWAYS CALL CLOSE() to End Segment After Tracing of Segment is Complete 299 func NewSegment(serviceNameOrUrl string, parentSegment ...*XRayParentSegment) *XSegment { 300 if util.LenTrim(serviceNameOrUrl) == 0 { 301 serviceNameOrUrl = "no.service.name.defined" 302 } 303 304 ctx, seg := xray.BeginSegment(context.Background(), serviceNameOrUrl) 305 306 if seg != nil && len(parentSegment) > 0 { 307 if parentSegment[0] != nil { 308 seg.ParentID = parentSegment[0].SegmentID 309 seg.TraceID = parentSegment[0].TraceID 310 } 311 } 312 313 return &XSegment{ 314 Ctx: ctx, 315 Seg: seg, 316 _segReady: true, 317 } 318 } 319 320 // NewSegmentNullable returns a new segment for the named service or url, if _xrayServiceOn = true, 321 // otherwise, nil is returned for *XSegment. 322 func NewSegmentNullable(serviceNameOrUrl string, parentSegment ...*XRayParentSegment) *XSegment { 323 _mu.RLock() 324 defer _mu.RUnlock() 325 if _xrayServiceOn { 326 return NewSegment(serviceNameOrUrl, parentSegment...) 327 } else { 328 return nil 329 } 330 } 331 332 // NewSegmentFromHeader begins a new segment for a named service or url based on http request, 333 // the http.Request Context is used as the base context when creating a new segment 334 // 335 // NOTE = ALWAYS CALL CLOSE() to End Segment After Tracing of Segment is Complete 336 func NewSegmentFromHeader(req *http.Request, traceHeaderName ...string) *XSegment { 337 if req == nil { 338 return &XSegment{ 339 Ctx: context.Background(), 340 Seg: nil, 341 _segReady: false, 342 } 343 } 344 345 name := GetXRayHttpRequestName(req) 346 347 if util.LenTrim(name) == 0 { 348 return &XSegment{ 349 Ctx: context.Background(), 350 Seg: nil, 351 _segReady: false, 352 } 353 } 354 355 hdr := GetXRayHeader(req, traceHeaderName...) 356 357 if hdr == nil { 358 return &XSegment{ 359 Ctx: context.Background(), 360 Seg: nil, 361 _segReady: false, 362 } 363 } 364 365 ctx, seg := xray.NewSegmentFromHeader(req.Context(), name, req, hdr) 366 367 return &XSegment{ 368 Ctx: ctx, 369 Seg: seg, 370 _segReady: true, 371 } 372 } 373 374 // SetParentSegment sets segment's ParentID and TraceID as indicated by input parameters 375 func (x *XSegment) SetParentSegment(parentID string, traceID string) { 376 if x._segReady && x.Seg != nil && x.Ctx != nil { 377 x.Seg.ParentID = parentID 378 x.Seg.TraceID = traceID 379 } 380 } 381 382 // NewSubSegment begins a new subsegment under the parent segment context, 383 // the subSegmentName defines a descriptive name of this sub segment for tracing, 384 // subsegment.Close(nil) should be called before its parent segment Close is called 385 // 386 // NOTE = ALWAYS CALL CLOSE() to End Segment After Tracing of Segment is Complete 387 func (x *XSegment) NewSubSegment(subSegmentName string) *XSegment { 388 if !x._segReady { 389 return &XSegment{ 390 Ctx: context.Background(), 391 Seg: nil, 392 _segReady: false, 393 } 394 } 395 396 if util.LenTrim(subSegmentName) == 0 { 397 subSegmentName = "no.subsegment.name.defined" 398 } 399 400 ctx, seg := xray.BeginSubsegment(x.Ctx, subSegmentName) 401 402 return &XSegment{ 403 Ctx: ctx, 404 Seg: seg, 405 _segReady: true, 406 } 407 } 408 409 // Ready checks if segment is ready for operations 410 func (x *XSegment) Ready() bool { 411 if x.Ctx == nil || x.Seg == nil || !x._segReady { 412 return false 413 } else { 414 return true 415 } 416 } 417 418 // Close will close a segment (or subsegment), 419 // always close subsegments first before closing its parent segment 420 func (x *XSegment) Close() { 421 if x._segReady && x.Seg != nil { 422 x.Seg.Close(nil) 423 } 424 } 425 426 // Capture wraps xray.Capture, by beginning and closing a subsegment with traceName, 427 // and synchronously executes the provided executeFunc which contains source application's logic 428 // 429 // traceName = descriptive name for the tracing session being tracked 430 // executeFunc = custom logic to execute within capture tracing context (context is segment context) 431 // traceData = optional additional data to add to the trace (meta, annotation, error) 432 func (x *XSegment) Capture(traceName string, executeFunc func() error, traceData ...*XTraceData) { 433 if !x._segReady || x.Ctx == nil || x.Seg == nil { 434 return 435 } 436 437 if executeFunc == nil { 438 return 439 } 440 441 if util.LenTrim(traceName) == 0 { 442 traceName = "no.synchronous.trace.name.defined" 443 } 444 445 _ = xray.Capture(x.Ctx, traceName, func(ctx context.Context) error { 446 // execute logic 447 err := executeFunc() 448 449 // add additional trace data if any to xray 450 if len(traceData) > 0 { 451 if m := traceData[0]; m != nil { 452 if m.Meta != nil && len(m.Meta) > 0 { 453 for k, v := range m.Meta { 454 _ = xray.AddMetadata(ctx, k, v) 455 } 456 } 457 458 if m.Annotations != nil && len(m.Annotations) > 0 { 459 for k, v := range m.Annotations { 460 _ = xray.AddAnnotation(ctx, k, v) 461 } 462 } 463 464 if m.Errors != nil && len(m.Errors) > 0 { 465 for _, v := range m.Errors { 466 _ = xray.AddError(ctx, v) 467 } 468 } 469 } 470 } 471 472 if s := xray.GetSegment(ctx); s != nil { 473 if err != nil { 474 s.Error = true 475 _ = xray.AddError(ctx, err) 476 } 477 } 478 479 // always return nil 480 return nil 481 }) 482 } 483 484 // CaptureAsync wraps xray.CaptureAsync, by beginning and closing a subsegment with traceName, 485 // and Asynchronously executes the provided executeFunc which contains source application's logic 486 // note = use CaptureAsync when tracing goroutine (rather than Capture) 487 // note = do not manually call Capture within goroutine to ensure segment is flushed properly 488 // 489 // traceName = descriptive name for the tracing session being tracked 490 // executeFunc = custom logic to execute within capture tracing context (context is segment context) 491 // traceData = optional additional data to add to the trace (meta, annotation, error) 492 func (x *XSegment) CaptureAsync(traceName string, executeFunc func() error, traceData ...*XTraceData) { 493 if !x._segReady || x.Ctx == nil || x.Seg == nil { 494 return 495 } 496 497 if executeFunc == nil { 498 return 499 } 500 501 if util.LenTrim(traceName) == 0 { 502 traceName = "no.asynchronous.trace.name.defined" 503 } 504 505 xray.CaptureAsync(x.Ctx, traceName, func(ctx context.Context) error { 506 // execute logic 507 err := executeFunc() 508 509 // add additional trace data if any to xray 510 if len(traceData) > 0 { 511 if m := traceData[0]; m != nil { 512 if m.Meta != nil && len(m.Meta) > 0 { 513 for k, v := range m.Meta { 514 _ = xray.AddMetadata(ctx, k, v) 515 } 516 } 517 518 if m.Annotations != nil && len(m.Annotations) > 0 { 519 for k, v := range m.Annotations { 520 _ = xray.AddAnnotation(ctx, k, v) 521 } 522 } 523 524 if m.Errors != nil && len(m.Errors) > 0 { 525 for _, v := range m.Errors { 526 _ = xray.AddError(ctx, v) 527 } 528 } 529 } 530 } 531 532 if s := xray.GetSegment(ctx); s != nil && err != nil { 533 s.Error = true 534 _ = xray.AddError(ctx, err) 535 } 536 537 // always return nil 538 return nil 539 }) 540 }