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 }