github.com/newrelic/go-agent@v3.26.0+incompatible/internal/txn_cross_process.go (about)

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