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  }