github.com/lulzWill/go-agent@v2.1.2+incompatible/internal/txn_cross_process.go (about)

     1  package internal
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/lulzWill/go-agent/internal/cat"
    10  )
    11  
    12  // Bitfield values for the TxnCrossProcess.Type field.
    13  const (
    14  	txnCrossProcessSynthetics = (1 << 0)
    15  	txnCrossProcessInbound    = (1 << 1)
    16  	txnCrossProcessOutbound   = (1 << 2)
    17  )
    18  
    19  var (
    20  	// ErrAccountNotTrusted indicates that, while the inbound headers were valid,
    21  	// the account ID within them is not trusted by the user's application.
    22  	ErrAccountNotTrusted = errors.New("account not trusted")
    23  )
    24  
    25  // TxnCrossProcess contains the metadata required for CAT and Synthetics
    26  // headers, transaction events, and traces.
    27  type TxnCrossProcess struct {
    28  	// The user side switch controlling whether CAT is enabled or not.
    29  	Enabled bool
    30  
    31  	// Rather than copying in the entire ConnectReply, here are the fields that
    32  	// we need to support CAT.
    33  	CrossProcessID  []byte
    34  	EncodingKey     []byte
    35  	TrustedAccounts trustedAccountSet
    36  
    37  	// CAT state for a given transaction.
    38  	Type                uint8
    39  	ClientID            string
    40  	GUID                string
    41  	TripID              string
    42  	PathHash            string
    43  	AlternatePathHashes map[string]bool
    44  	ReferringPathHash   string
    45  	ReferringTxnGUID    string
    46  	Synthetics          *cat.SyntheticsHeader
    47  
    48  	// The encoded synthetics header received as part of the request headers, if
    49  	// any. By storing this here, we avoid needing to marshal the invariant
    50  	// Synthetics struct above each time an external segment is created.
    51  	SyntheticsHeader string
    52  }
    53  
    54  // CrossProcessMetadata represents the metadata that must be transmitted with
    55  // an external request for CAT to work.
    56  type CrossProcessMetadata struct {
    57  	ID         string
    58  	TxnData    string
    59  	Synthetics string
    60  }
    61  
    62  // Init initialises a TxnCrossProcess based on the given application connect
    63  // reply and metadata fields, if any.
    64  func (txp *TxnCrossProcess) Init(enabled bool, reply *ConnectReply, metadata CrossProcessMetadata) error {
    65  	txp.CrossProcessID = []byte(reply.CrossProcessID)
    66  	txp.EncodingKey = []byte(reply.EncodingKey)
    67  	txp.Enabled = enabled
    68  	txp.TrustedAccounts = reply.TrustedAccounts
    69  
    70  	return txp.handleInboundRequestHeaders(metadata)
    71  }
    72  
    73  // CreateCrossProcessMetadata generates request metadata that enable CAT and
    74  // Synthetics support for an external segment.
    75  func (txp *TxnCrossProcess) CreateCrossProcessMetadata(txnName, appName string) (CrossProcessMetadata, error) {
    76  	metadata := CrossProcessMetadata{}
    77  
    78  	// Regardless of the user's CAT settings, if there was a synthetics header in
    79  	// the inbound request, a synthetics header should always be included in the
    80  	// outbound request headers.
    81  	if txp.IsSynthetics() {
    82  		metadata.Synthetics = txp.SyntheticsHeader
    83  	}
    84  
    85  	if txp.Enabled {
    86  		txp.SetOutbound(true)
    87  		txp.requireTripID()
    88  
    89  		id, err := txp.outboundID()
    90  		if err != nil {
    91  			return metadata, err
    92  		}
    93  
    94  		txnData, err := txp.outboundTxnData(txnName, appName)
    95  		if err != nil {
    96  			return metadata, err
    97  		}
    98  
    99  		metadata.ID = id
   100  		metadata.TxnData = txnData
   101  	}
   102  
   103  	return metadata, nil
   104  }
   105  
   106  // Finalise handles any end-of-transaction tasks. In practice, this simply
   107  // means ensuring the path hash is set if it hasn't already been.
   108  func (txp *TxnCrossProcess) Finalise(txnName, appName string) error {
   109  	if txp.Used() {
   110  		_, err := txp.setPathHash(txnName, appName)
   111  		return err
   112  	}
   113  
   114  	// If there was no CAT activity, then do nothing, successfully.
   115  	return nil
   116  }
   117  
   118  // IsInbound returns true if the transaction had inbound CAT headers.
   119  func (txp *TxnCrossProcess) IsInbound() bool {
   120  	return 0 != (txp.Type & txnCrossProcessInbound)
   121  }
   122  
   123  // IsOutbound returns true if the transaction has generated outbound CAT
   124  // headers.
   125  func (txp *TxnCrossProcess) IsOutbound() bool {
   126  	// We don't actually use this anywhere today, but it feels weird not having
   127  	// it.
   128  	return 0 != (txp.Type & txnCrossProcessOutbound)
   129  }
   130  
   131  // IsSynthetics returns true if the transaction had inbound Synthetics headers.
   132  func (txp *TxnCrossProcess) IsSynthetics() bool {
   133  	// Technically, this is redundant: the presence of a non-nil Synthetics
   134  	// pointer should be sufficient to determine if this is a synthetics
   135  	// transaction. Nevertheless, it's convenient to have the Type field be
   136  	// non-zero if any CAT behaviour has occurred.
   137  	return 0 != (txp.Type&txnCrossProcessSynthetics) && nil != txp.Synthetics
   138  }
   139  
   140  // ParseAppData decodes the given appData value.
   141  func (txp *TxnCrossProcess) ParseAppData(encodedAppData string) (*cat.AppDataHeader, error) {
   142  	if !txp.Enabled {
   143  		return nil, nil
   144  	}
   145  	if encodedAppData != "" {
   146  		rawAppData, err := deobfuscate(encodedAppData, txp.EncodingKey)
   147  		if err != nil {
   148  			return nil, err
   149  		}
   150  
   151  		appData := &cat.AppDataHeader{}
   152  		if err := json.Unmarshal(rawAppData, appData); err != nil {
   153  			return nil, err
   154  		}
   155  
   156  		return appData, nil
   157  	}
   158  
   159  	return nil, nil
   160  }
   161  
   162  // CreateAppData creates the appData value that should be sent with a response
   163  // to ensure CAT operates as expected.
   164  func (txp *TxnCrossProcess) CreateAppData(name string, queueTime, responseTime time.Duration, contentLength int64) (string, error) {
   165  	// If CAT is disabled, do nothing, successfully.
   166  	if !txp.Enabled {
   167  		return "", nil
   168  	}
   169  
   170  	data, err := json.Marshal(&cat.AppDataHeader{
   171  		CrossProcessID:        string(txp.CrossProcessID),
   172  		TransactionName:       name,
   173  		QueueTimeInSeconds:    queueTime.Seconds(),
   174  		ResponseTimeInSeconds: responseTime.Seconds(),
   175  		ContentLength:         contentLength,
   176  		TransactionGUID:       txp.GUID,
   177  	})
   178  	if err != nil {
   179  		return "", err
   180  	}
   181  
   182  	obfuscated, err := obfuscate(data, txp.EncodingKey)
   183  	if err != nil {
   184  		return "", err
   185  	}
   186  
   187  	return obfuscated, nil
   188  }
   189  
   190  // Used returns true if any CAT or Synthetics related functionality has been
   191  // triggered on the transaction.
   192  func (txp *TxnCrossProcess) Used() bool {
   193  	return 0 != txp.Type
   194  }
   195  
   196  // SetInbound sets the inbound CAT flag. This function is provided only for
   197  // internal and unit testing purposes, and should not be used outside of this
   198  // package normally.
   199  func (txp *TxnCrossProcess) SetInbound(inbound bool) {
   200  	if inbound {
   201  		txp.Type |= txnCrossProcessInbound
   202  	} else {
   203  		txp.Type &^= txnCrossProcessInbound
   204  	}
   205  }
   206  
   207  // SetOutbound sets the outbound CAT flag. This function is provided only for
   208  // internal and unit testing purposes, and should not be used outside of this
   209  // package normally.
   210  func (txp *TxnCrossProcess) SetOutbound(outbound bool) {
   211  	if outbound {
   212  		txp.Type |= txnCrossProcessOutbound
   213  	} else {
   214  		txp.Type &^= txnCrossProcessOutbound
   215  	}
   216  }
   217  
   218  // SetSynthetics sets the Synthetics CAT flag. This function is provided only
   219  // for internal and unit testing purposes, and should not be used outside of
   220  // this package normally.
   221  func (txp *TxnCrossProcess) SetSynthetics(synthetics bool) {
   222  	if synthetics {
   223  		txp.Type |= txnCrossProcessSynthetics
   224  	} else {
   225  		txp.Type &^= txnCrossProcessSynthetics
   226  	}
   227  }
   228  
   229  // handleInboundRequestHeaders parses the CAT headers from the given metadata
   230  // and updates the relevant fields on the provided TxnData.
   231  func (txp *TxnCrossProcess) handleInboundRequestHeaders(metadata CrossProcessMetadata) error {
   232  	if txp.Enabled && metadata.ID != "" && metadata.TxnData != "" {
   233  		if err := txp.handleInboundRequestEncodedCAT(metadata.ID, metadata.TxnData); err != nil {
   234  			return err
   235  		}
   236  	}
   237  
   238  	if metadata.Synthetics != "" {
   239  		if err := txp.handleInboundRequestEncodedSynthetics(metadata.Synthetics); err != nil {
   240  			return err
   241  		}
   242  	}
   243  
   244  	return nil
   245  }
   246  
   247  func (txp *TxnCrossProcess) handleInboundRequestEncodedCAT(encodedID, encodedTxnData string) error {
   248  	rawID, err := deobfuscate(encodedID, txp.EncodingKey)
   249  	if err != nil {
   250  		return err
   251  	}
   252  
   253  	rawTxnData, err := deobfuscate(encodedTxnData, txp.EncodingKey)
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	if err := txp.handleInboundRequestID(rawID); err != nil {
   259  		return err
   260  	}
   261  
   262  	return txp.handleInboundRequestTxnData(rawTxnData)
   263  }
   264  
   265  func (txp *TxnCrossProcess) handleInboundRequestID(raw []byte) error {
   266  	id, err := cat.NewIDHeader(raw)
   267  	if err != nil {
   268  		return err
   269  	}
   270  
   271  	if !txp.TrustedAccounts.IsTrusted(id.AccountID) {
   272  		return ErrAccountNotTrusted
   273  	}
   274  
   275  	txp.SetInbound(true)
   276  	txp.ClientID = string(raw)
   277  	txp.setRequireGUID()
   278  
   279  	return nil
   280  }
   281  
   282  func (txp *TxnCrossProcess) handleInboundRequestTxnData(raw []byte) error {
   283  	txnData := &cat.TxnDataHeader{}
   284  	if err := json.Unmarshal(raw, txnData); err != nil {
   285  		return err
   286  	}
   287  
   288  	txp.SetInbound(true)
   289  	if txnData.TripID != "" {
   290  		txp.TripID = txnData.TripID
   291  	} else {
   292  		txp.setRequireGUID()
   293  		txp.TripID = txp.GUID
   294  	}
   295  	txp.ReferringTxnGUID = txnData.GUID
   296  	txp.ReferringPathHash = txnData.PathHash
   297  
   298  	return nil
   299  }
   300  
   301  func (txp *TxnCrossProcess) handleInboundRequestEncodedSynthetics(encoded string) error {
   302  	raw, err := deobfuscate(encoded, txp.EncodingKey)
   303  	if err != nil {
   304  		return err
   305  	}
   306  
   307  	if err := txp.handleInboundRequestSynthetics(raw); err != nil {
   308  		return err
   309  	}
   310  
   311  	txp.SyntheticsHeader = encoded
   312  	return nil
   313  }
   314  
   315  func (txp *TxnCrossProcess) handleInboundRequestSynthetics(raw []byte) error {
   316  	synthetics := &cat.SyntheticsHeader{}
   317  	if err := json.Unmarshal(raw, synthetics); err != nil {
   318  		return err
   319  	}
   320  
   321  	// The specced behaviour here if the account isn't trusted is to disable the
   322  	// synthetics handling, but not CAT in general, so we won't return an error
   323  	// here.
   324  	if txp.TrustedAccounts.IsTrusted(synthetics.AccountID) {
   325  		txp.SetSynthetics(true)
   326  		txp.setRequireGUID()
   327  		txp.Synthetics = synthetics
   328  	}
   329  
   330  	return nil
   331  }
   332  
   333  func (txp *TxnCrossProcess) outboundID() (string, error) {
   334  	return obfuscate(txp.CrossProcessID, txp.EncodingKey)
   335  }
   336  
   337  func (txp *TxnCrossProcess) outboundTxnData(txnName, appName string) (string, error) {
   338  	pathHash, err := txp.setPathHash(txnName, appName)
   339  	if err != nil {
   340  		return "", err
   341  	}
   342  
   343  	data, err := json.Marshal(&cat.TxnDataHeader{
   344  		GUID:     txp.GUID,
   345  		TripID:   txp.TripID,
   346  		PathHash: pathHash,
   347  	})
   348  	if err != nil {
   349  		return "", err
   350  	}
   351  
   352  	return obfuscate(data, txp.EncodingKey)
   353  }
   354  
   355  // setRequireGUID ensures that the transaction has a valid GUID, and sets the
   356  // GUID and trip ID if they are not already set.
   357  func (txp *TxnCrossProcess) setRequireGUID() {
   358  	if txp.GUID != "" {
   359  		return
   360  	}
   361  
   362  	txp.GUID = fmt.Sprintf("%x", RandUint64())
   363  
   364  	if txp.TripID == "" {
   365  		txp.TripID = txp.GUID
   366  	}
   367  }
   368  
   369  // requireTripID ensures that the transaction has a valid trip ID.
   370  func (txp *TxnCrossProcess) requireTripID() {
   371  	if txp.TripID != "" {
   372  		return
   373  	}
   374  
   375  	txp.setRequireGUID()
   376  	txp.TripID = txp.GUID
   377  }
   378  
   379  // setPathHash generates a path hash, sets the transaction's path hash to
   380  // match, and returns it. This function will also ensure that the alternate
   381  // path hashes are correctly updated.
   382  func (txp *TxnCrossProcess) setPathHash(txnName, appName string) (string, error) {
   383  	pathHash, err := cat.GeneratePathHash(txp.ReferringPathHash, txnName, appName)
   384  	if err != nil {
   385  		return "", err
   386  	}
   387  
   388  	if pathHash != txp.PathHash {
   389  		if txp.PathHash != "" {
   390  			// Lazily initialise the alternate path hashes if they haven't been
   391  			// already.
   392  			if txp.AlternatePathHashes == nil {
   393  				txp.AlternatePathHashes = make(map[string]bool)
   394  			}
   395  
   396  			// The spec limits us to a maximum of 10 alternate path hashes.
   397  			if len(txp.AlternatePathHashes) < 10 {
   398  				txp.AlternatePathHashes[txp.PathHash] = true
   399  			}
   400  		}
   401  		txp.PathHash = pathHash
   402  	}
   403  
   404  	return pathHash, nil
   405  }