github.com/newrelic/go-agent@v3.26.0+incompatible/internal/txn_events.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  	"bytes"
     8  	"sort"
     9  	"strings"
    10  	"time"
    11  )
    12  
    13  // DatastoreExternalTotals contains overview of external and datastore calls
    14  // made during a transaction.
    15  type DatastoreExternalTotals struct {
    16  	externalCallCount  uint64
    17  	externalDuration   time.Duration
    18  	datastoreCallCount uint64
    19  	datastoreDuration  time.Duration
    20  }
    21  
    22  // WriteJSON prepares JSON in the format expected by the collector.
    23  func (e *TxnEvent) WriteJSON(buf *bytes.Buffer) {
    24  	w := jsonFieldsWriter{buf: buf}
    25  	buf.WriteByte('[')
    26  	buf.WriteByte('{')
    27  	w.stringField("type", "Transaction")
    28  	w.stringField("name", e.FinalName)
    29  	w.floatField("timestamp", timeToFloatSeconds(e.Start))
    30  	if ApdexNone != e.Zone {
    31  		w.stringField("nr.apdexPerfZone", e.Zone.label())
    32  	}
    33  
    34  	w.boolField("error", e.HasError)
    35  
    36  	sharedTransactionIntrinsics(e, &w)
    37  
    38  	// totalTime gets put into transaction events but not error events:
    39  	// https://source.datanerd.us/agents/agent-specs/blob/master/Total-Time-Async.md#attributes
    40  	w.floatField("totalTime", e.TotalTime.Seconds())
    41  
    42  	// Write better CAT intrinsics if enabled
    43  	sharedBetterCATIntrinsics(e, &w)
    44  
    45  	if e.BetterCAT.Enabled {
    46  		if p := e.BetterCAT.Inbound; nil != p {
    47  			if "" != p.TransactionID {
    48  				w.stringField("parentId", p.TransactionID)
    49  			}
    50  
    51  			if "" != p.ID {
    52  				w.stringField("parentSpanId", p.ID)
    53  			}
    54  		}
    55  	}
    56  
    57  	// Write old CAT intrinsics if enabled
    58  	oldCATIntrinsics(e, &w)
    59  
    60  	buf.WriteByte('}')
    61  	buf.WriteByte(',')
    62  	userAttributesJSON(e.Attrs, buf, destTxnEvent, nil)
    63  	buf.WriteByte(',')
    64  	agentAttributesJSON(e.Attrs, buf, destTxnEvent)
    65  	buf.WriteByte(']')
    66  }
    67  
    68  // oldCATIntrinsics reports old CAT intrinsics for Transaction
    69  // if CrossProcess.Used() is true
    70  func oldCATIntrinsics(e *TxnEvent, w *jsonFieldsWriter) {
    71  	if !e.CrossProcess.Used() {
    72  		return
    73  	}
    74  
    75  	if e.CrossProcess.ClientID != "" {
    76  		w.stringField("client_cross_process_id", e.CrossProcess.ClientID)
    77  	}
    78  	if e.CrossProcess.TripID != "" {
    79  		w.stringField("nr.tripId", e.CrossProcess.TripID)
    80  	}
    81  	if e.CrossProcess.PathHash != "" {
    82  		w.stringField("nr.pathHash", e.CrossProcess.PathHash)
    83  	}
    84  	if e.CrossProcess.ReferringPathHash != "" {
    85  		w.stringField("nr.referringPathHash", e.CrossProcess.ReferringPathHash)
    86  	}
    87  	if e.CrossProcess.GUID != "" {
    88  		w.stringField("nr.guid", e.CrossProcess.GUID)
    89  	}
    90  	if e.CrossProcess.ReferringTxnGUID != "" {
    91  		w.stringField("nr.referringTransactionGuid", e.CrossProcess.ReferringTxnGUID)
    92  	}
    93  	if len(e.CrossProcess.AlternatePathHashes) > 0 {
    94  		hashes := make([]string, 0, len(e.CrossProcess.AlternatePathHashes))
    95  		for hash := range e.CrossProcess.AlternatePathHashes {
    96  			hashes = append(hashes, hash)
    97  		}
    98  		sort.Strings(hashes)
    99  		w.stringField("nr.alternatePathHashes", strings.Join(hashes, ","))
   100  	}
   101  }
   102  
   103  // sharedTransactionIntrinsics reports intrinsics that are shared
   104  // by Transaction and TransactionError
   105  func sharedTransactionIntrinsics(e *TxnEvent, w *jsonFieldsWriter) {
   106  	w.floatField("duration", e.Duration.Seconds())
   107  	if e.Queuing > 0 {
   108  		w.floatField("queueDuration", e.Queuing.Seconds())
   109  	}
   110  	if e.externalCallCount > 0 {
   111  		w.intField("externalCallCount", int64(e.externalCallCount))
   112  		w.floatField("externalDuration", e.externalDuration.Seconds())
   113  	}
   114  	if e.datastoreCallCount > 0 {
   115  		// Note that "database" is used for the keys here instead of
   116  		// "datastore" for historical reasons.
   117  		w.intField("databaseCallCount", int64(e.datastoreCallCount))
   118  		w.floatField("databaseDuration", e.datastoreDuration.Seconds())
   119  	}
   120  
   121  	if e.CrossProcess.IsSynthetics() {
   122  		w.stringField("nr.syntheticsResourceId", e.CrossProcess.Synthetics.ResourceID)
   123  		w.stringField("nr.syntheticsJobId", e.CrossProcess.Synthetics.JobID)
   124  		w.stringField("nr.syntheticsMonitorId", e.CrossProcess.Synthetics.MonitorID)
   125  	}
   126  }
   127  
   128  // sharedBetterCATIntrinsics reports intrinsics that are shared
   129  // by Transaction, TransactionError, and Slow SQL
   130  func sharedBetterCATIntrinsics(e *TxnEvent, w *jsonFieldsWriter) {
   131  	if e.BetterCAT.Enabled {
   132  		if p := e.BetterCAT.Inbound; nil != p {
   133  			w.stringField("parent.type", p.Type)
   134  			w.stringField("parent.app", p.App)
   135  			w.stringField("parent.account", p.Account)
   136  			w.stringField("parent.transportType", p.TransportType)
   137  			w.floatField("parent.transportDuration", p.TransportDuration.Seconds())
   138  		}
   139  
   140  		w.stringField("guid", e.BetterCAT.ID)
   141  		w.stringField("traceId", e.BetterCAT.TraceID())
   142  		w.writerField("priority", e.BetterCAT.Priority)
   143  		w.boolField("sampled", e.BetterCAT.Sampled)
   144  	}
   145  }
   146  
   147  // MarshalJSON is used for testing.
   148  func (e *TxnEvent) MarshalJSON() ([]byte, error) {
   149  	buf := bytes.NewBuffer(make([]byte, 0, 256))
   150  
   151  	e.WriteJSON(buf)
   152  
   153  	return buf.Bytes(), nil
   154  }
   155  
   156  type txnEvents struct {
   157  	*analyticsEvents
   158  }
   159  
   160  func newTxnEvents(max int) *txnEvents {
   161  	return &txnEvents{
   162  		analyticsEvents: newAnalyticsEvents(max),
   163  	}
   164  }
   165  
   166  func (events *txnEvents) AddTxnEvent(e *TxnEvent, priority Priority) {
   167  	// Synthetics events always get priority: normal event priorities are in the
   168  	// range [0.0,1.99999], so adding 2 means that a Synthetics event will always
   169  	// win.
   170  	if e.CrossProcess.IsSynthetics() {
   171  		priority += 2.0
   172  	}
   173  	events.addEvent(analyticsEvent{priority: priority, jsonWriter: e})
   174  }
   175  
   176  func (events *txnEvents) MergeIntoHarvest(h *Harvest) {
   177  	h.TxnEvents.mergeFailed(events.analyticsEvents)
   178  }
   179  
   180  func (events *txnEvents) Data(agentRunID string, harvestStart time.Time) ([]byte, error) {
   181  	return events.CollectorJSON(agentRunID)
   182  }
   183  
   184  func (events *txnEvents) EndpointMethod() string {
   185  	return cmdTxnEvents
   186  }
   187  
   188  func (events *txnEvents) payloads(limit int) []PayloadCreator {
   189  	if events.NumSaved() < float64(limit) {
   190  		return []PayloadCreator{events}
   191  	}
   192  	e1, e2 := events.split()
   193  	return []PayloadCreator{
   194  		&txnEvents{analyticsEvents: e1},
   195  		&txnEvents{analyticsEvents: e2},
   196  	}
   197  }