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 }