493 lines
11 KiB
Go
493 lines
11 KiB
Go
package newrelic
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/newrelic/go-agent/internal"
|
|
)
|
|
|
|
type txnInput struct {
|
|
W http.ResponseWriter
|
|
Request *http.Request
|
|
Config Config
|
|
Reply *internal.ConnectReply
|
|
Consumer dataConsumer
|
|
attrConfig *internal.AttributeConfig
|
|
}
|
|
|
|
type txn struct {
|
|
txnInput
|
|
// This mutex is required since the consumer may call the public API
|
|
// interface functions from different routines.
|
|
sync.Mutex
|
|
// finished indicates whether or not End() has been called. After
|
|
// finished has been set to true, no recording should occur.
|
|
finished bool
|
|
queuing time.Duration
|
|
start time.Time
|
|
name string // Work in progress name
|
|
isWeb bool
|
|
ignore bool
|
|
errors internal.TxnErrors // Lazily initialized.
|
|
attrs *internal.Attributes
|
|
|
|
// Fields relating to tracing and breakdown metrics/segments.
|
|
tracer internal.Tracer
|
|
|
|
// wroteHeader prevents capturing multiple response code errors if the
|
|
// user erroneously calls WriteHeader multiple times.
|
|
wroteHeader bool
|
|
|
|
// Fields assigned at completion
|
|
stop time.Time
|
|
duration time.Duration
|
|
finalName string // Full finalized metric name
|
|
zone internal.ApdexZone
|
|
apdexThreshold time.Duration
|
|
}
|
|
|
|
func newTxn(input txnInput, name string) *txn {
|
|
txn := &txn{
|
|
txnInput: input,
|
|
start: time.Now(),
|
|
name: name,
|
|
isWeb: nil != input.Request,
|
|
attrs: internal.NewAttributes(input.attrConfig),
|
|
}
|
|
if nil != txn.Request {
|
|
txn.queuing = internal.QueueDuration(input.Request.Header, txn.start)
|
|
internal.RequestAgentAttributes(txn.attrs, input.Request)
|
|
}
|
|
txn.attrs.Agent.HostDisplayName = txn.Config.HostDisplayName
|
|
txn.tracer.Enabled = txn.txnTracesEnabled()
|
|
txn.tracer.SegmentThreshold = txn.Config.TransactionTracer.SegmentThreshold
|
|
txn.tracer.StackTraceThreshold = txn.Config.TransactionTracer.StackTraceThreshold
|
|
txn.tracer.SlowQueriesEnabled = txn.slowQueriesEnabled()
|
|
txn.tracer.SlowQueryThreshold = txn.Config.DatastoreTracer.SlowQuery.Threshold
|
|
|
|
return txn
|
|
}
|
|
|
|
func (txn *txn) slowQueriesEnabled() bool {
|
|
return txn.Config.DatastoreTracer.SlowQuery.Enabled &&
|
|
txn.Reply.CollectTraces
|
|
}
|
|
|
|
func (txn *txn) txnTracesEnabled() bool {
|
|
return txn.Config.TransactionTracer.Enabled &&
|
|
txn.Reply.CollectTraces
|
|
}
|
|
|
|
func (txn *txn) txnEventsEnabled() bool {
|
|
return txn.Config.TransactionEvents.Enabled &&
|
|
txn.Reply.CollectAnalyticsEvents
|
|
}
|
|
|
|
func (txn *txn) errorEventsEnabled() bool {
|
|
return txn.Config.ErrorCollector.CaptureEvents &&
|
|
txn.Reply.CollectErrorEvents
|
|
}
|
|
|
|
func (txn *txn) freezeName() {
|
|
if txn.ignore || ("" != txn.finalName) {
|
|
return
|
|
}
|
|
|
|
txn.finalName = internal.CreateFullTxnName(txn.name, txn.Reply, txn.isWeb)
|
|
if "" == txn.finalName {
|
|
txn.ignore = true
|
|
}
|
|
}
|
|
|
|
func (txn *txn) getsApdex() bool {
|
|
return txn.isWeb
|
|
}
|
|
|
|
func (txn *txn) txnTraceThreshold() time.Duration {
|
|
if txn.Config.TransactionTracer.Threshold.IsApdexFailing {
|
|
return internal.ApdexFailingThreshold(txn.apdexThreshold)
|
|
}
|
|
return txn.Config.TransactionTracer.Threshold.Duration
|
|
}
|
|
|
|
func (txn *txn) shouldSaveTrace() bool {
|
|
return txn.txnTracesEnabled() &&
|
|
(txn.duration >= txn.txnTraceThreshold())
|
|
}
|
|
|
|
func (txn *txn) hasErrors() bool {
|
|
return len(txn.errors) > 0
|
|
}
|
|
|
|
func (txn *txn) MergeIntoHarvest(h *internal.Harvest) {
|
|
exclusive := time.Duration(0)
|
|
children := internal.TracerRootChildren(&txn.tracer)
|
|
if txn.duration > children {
|
|
exclusive = txn.duration - children
|
|
}
|
|
|
|
internal.CreateTxnMetrics(internal.CreateTxnMetricsArgs{
|
|
IsWeb: txn.isWeb,
|
|
Duration: txn.duration,
|
|
Exclusive: exclusive,
|
|
Name: txn.finalName,
|
|
Zone: txn.zone,
|
|
ApdexThreshold: txn.apdexThreshold,
|
|
HasErrors: txn.hasErrors(),
|
|
Queueing: txn.queuing,
|
|
}, h.Metrics)
|
|
|
|
internal.MergeBreakdownMetrics(&txn.tracer, h.Metrics, txn.finalName, txn.isWeb)
|
|
|
|
if txn.txnEventsEnabled() {
|
|
h.TxnEvents.AddTxnEvent(&internal.TxnEvent{
|
|
Name: txn.finalName,
|
|
Timestamp: txn.start,
|
|
Duration: txn.duration,
|
|
Queuing: txn.queuing,
|
|
Zone: txn.zone,
|
|
Attrs: txn.attrs,
|
|
DatastoreExternalTotals: txn.tracer.DatastoreExternalTotals,
|
|
})
|
|
}
|
|
|
|
requestURI := ""
|
|
if nil != txn.Request && nil != txn.Request.URL {
|
|
requestURI = internal.SafeURL(txn.Request.URL)
|
|
}
|
|
|
|
internal.MergeTxnErrors(h.ErrorTraces, txn.errors, txn.finalName, requestURI, txn.attrs)
|
|
|
|
if txn.errorEventsEnabled() {
|
|
for _, e := range txn.errors {
|
|
h.ErrorEvents.Add(&internal.ErrorEvent{
|
|
Klass: e.Klass,
|
|
Msg: e.Msg,
|
|
When: e.When,
|
|
TxnName: txn.finalName,
|
|
Duration: txn.duration,
|
|
Queuing: txn.queuing,
|
|
Attrs: txn.attrs,
|
|
DatastoreExternalTotals: txn.tracer.DatastoreExternalTotals,
|
|
})
|
|
}
|
|
}
|
|
|
|
if txn.shouldSaveTrace() {
|
|
h.TxnTraces.Witness(internal.HarvestTrace{
|
|
Start: txn.start,
|
|
Duration: txn.duration,
|
|
MetricName: txn.finalName,
|
|
CleanURL: requestURI,
|
|
Trace: txn.tracer.TxnTrace,
|
|
ForcePersist: false,
|
|
GUID: "",
|
|
SyntheticsResourceID: "",
|
|
Attrs: txn.attrs,
|
|
})
|
|
}
|
|
|
|
if nil != txn.tracer.SlowQueries {
|
|
h.SlowSQLs.Merge(txn.tracer.SlowQueries, txn.finalName, requestURI)
|
|
}
|
|
}
|
|
|
|
func responseCodeIsError(cfg *Config, code int) bool {
|
|
if code < http.StatusBadRequest { // 400
|
|
return false
|
|
}
|
|
for _, ignoreCode := range cfg.ErrorCollector.IgnoreStatusCodes {
|
|
if code == ignoreCode {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func headersJustWritten(txn *txn, code int) {
|
|
if txn.finished {
|
|
return
|
|
}
|
|
if txn.wroteHeader {
|
|
return
|
|
}
|
|
txn.wroteHeader = true
|
|
|
|
internal.ResponseHeaderAttributes(txn.attrs, txn.W.Header())
|
|
internal.ResponseCodeAttribute(txn.attrs, code)
|
|
|
|
if responseCodeIsError(&txn.Config, code) {
|
|
e := internal.TxnErrorFromResponseCode(time.Now(), code)
|
|
e.Stack = internal.GetStackTrace(1)
|
|
txn.noticeErrorInternal(e)
|
|
}
|
|
}
|
|
|
|
func (txn *txn) Header() http.Header { return txn.W.Header() }
|
|
|
|
func (txn *txn) Write(b []byte) (int, error) {
|
|
n, err := txn.W.Write(b)
|
|
|
|
txn.Lock()
|
|
defer txn.Unlock()
|
|
|
|
headersJustWritten(txn, http.StatusOK)
|
|
|
|
return n, err
|
|
}
|
|
|
|
func (txn *txn) WriteHeader(code int) {
|
|
txn.W.WriteHeader(code)
|
|
|
|
txn.Lock()
|
|
defer txn.Unlock()
|
|
|
|
headersJustWritten(txn, code)
|
|
}
|
|
|
|
func (txn *txn) End() error {
|
|
txn.Lock()
|
|
defer txn.Unlock()
|
|
|
|
if txn.finished {
|
|
return errAlreadyEnded
|
|
}
|
|
|
|
txn.finished = true
|
|
|
|
r := recover()
|
|
if nil != r {
|
|
e := internal.TxnErrorFromPanic(time.Now(), r)
|
|
e.Stack = internal.GetStackTrace(0)
|
|
txn.noticeErrorInternal(e)
|
|
}
|
|
|
|
txn.stop = time.Now()
|
|
txn.duration = txn.stop.Sub(txn.start)
|
|
|
|
txn.freezeName()
|
|
|
|
// Assign apdexThreshold regardless of whether or not the transaction
|
|
// gets apdex since it may be used to calculate the trace threshold.
|
|
txn.apdexThreshold = internal.CalculateApdexThreshold(txn.Reply, txn.finalName)
|
|
|
|
if txn.getsApdex() {
|
|
if txn.hasErrors() {
|
|
txn.zone = internal.ApdexFailing
|
|
} else {
|
|
txn.zone = internal.CalculateApdexZone(txn.apdexThreshold, txn.duration)
|
|
}
|
|
} else {
|
|
txn.zone = internal.ApdexNone
|
|
}
|
|
|
|
if txn.Config.Logger.DebugEnabled() {
|
|
txn.Config.Logger.Debug("transaction ended", map[string]interface{}{
|
|
"name": txn.finalName,
|
|
"duration_ms": txn.duration.Seconds() * 1000.0,
|
|
"ignored": txn.ignore,
|
|
"run": txn.Reply.RunID,
|
|
})
|
|
}
|
|
|
|
if !txn.ignore {
|
|
txn.Consumer.Consume(txn.Reply.RunID, txn)
|
|
}
|
|
|
|
// Note that if a consumer uses `panic(nil)`, the panic will not
|
|
// propagate.
|
|
if nil != r {
|
|
panic(r)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (txn *txn) AddAttribute(name string, value interface{}) error {
|
|
txn.Lock()
|
|
defer txn.Unlock()
|
|
|
|
if txn.finished {
|
|
return errAlreadyEnded
|
|
}
|
|
|
|
return internal.AddUserAttribute(txn.attrs, name, value, internal.DestAll)
|
|
}
|
|
|
|
var (
|
|
errorsLocallyDisabled = errors.New("errors locally disabled")
|
|
errorsRemotelyDisabled = errors.New("errors remotely disabled")
|
|
errNilError = errors.New("nil error")
|
|
errAlreadyEnded = errors.New("transaction has already ended")
|
|
)
|
|
|
|
const (
|
|
highSecurityErrorMsg = "message removed by high security setting"
|
|
)
|
|
|
|
func (txn *txn) noticeErrorInternal(err internal.TxnError) error {
|
|
if !txn.Config.ErrorCollector.Enabled {
|
|
return errorsLocallyDisabled
|
|
}
|
|
|
|
if !txn.Reply.CollectErrors {
|
|
return errorsRemotelyDisabled
|
|
}
|
|
|
|
if nil == txn.errors {
|
|
txn.errors = internal.NewTxnErrors(internal.MaxTxnErrors)
|
|
}
|
|
|
|
if txn.Config.HighSecurity {
|
|
err.Msg = highSecurityErrorMsg
|
|
}
|
|
|
|
txn.errors.Add(err)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (txn *txn) NoticeError(err error) error {
|
|
txn.Lock()
|
|
defer txn.Unlock()
|
|
|
|
if txn.finished {
|
|
return errAlreadyEnded
|
|
}
|
|
|
|
if nil == err {
|
|
return errNilError
|
|
}
|
|
|
|
e := internal.TxnErrorFromError(time.Now(), err)
|
|
e.Stack = internal.GetStackTrace(2)
|
|
return txn.noticeErrorInternal(e)
|
|
}
|
|
|
|
func (txn *txn) SetName(name string) error {
|
|
txn.Lock()
|
|
defer txn.Unlock()
|
|
|
|
if txn.finished {
|
|
return errAlreadyEnded
|
|
}
|
|
|
|
txn.name = name
|
|
return nil
|
|
}
|
|
|
|
func (txn *txn) Ignore() error {
|
|
txn.Lock()
|
|
defer txn.Unlock()
|
|
|
|
if txn.finished {
|
|
return errAlreadyEnded
|
|
}
|
|
txn.ignore = true
|
|
return nil
|
|
}
|
|
|
|
func (txn *txn) StartSegmentNow() SegmentStartTime {
|
|
var s internal.SegmentStartTime
|
|
txn.Lock()
|
|
if !txn.finished {
|
|
s = internal.StartSegment(&txn.tracer, time.Now())
|
|
}
|
|
txn.Unlock()
|
|
return SegmentStartTime{
|
|
segment: segment{
|
|
start: s,
|
|
txn: txn,
|
|
},
|
|
}
|
|
}
|
|
|
|
type segment struct {
|
|
start internal.SegmentStartTime
|
|
txn *txn
|
|
}
|
|
|
|
func endSegment(s Segment) {
|
|
txn := s.StartTime.txn
|
|
if nil == txn {
|
|
return
|
|
}
|
|
txn.Lock()
|
|
if !txn.finished {
|
|
internal.EndBasicSegment(&txn.tracer, s.StartTime.start, time.Now(), s.Name)
|
|
}
|
|
txn.Unlock()
|
|
}
|
|
|
|
func endDatastore(s DatastoreSegment) {
|
|
txn := s.StartTime.txn
|
|
if nil == txn {
|
|
return
|
|
}
|
|
txn.Lock()
|
|
defer txn.Unlock()
|
|
|
|
if txn.finished {
|
|
return
|
|
}
|
|
if txn.Config.HighSecurity {
|
|
s.QueryParameters = nil
|
|
}
|
|
if !txn.Config.DatastoreTracer.QueryParameters.Enabled {
|
|
s.QueryParameters = nil
|
|
}
|
|
if !txn.Config.DatastoreTracer.DatabaseNameReporting.Enabled {
|
|
s.DatabaseName = ""
|
|
}
|
|
if !txn.Config.DatastoreTracer.InstanceReporting.Enabled {
|
|
s.Host = ""
|
|
s.PortPathOrID = ""
|
|
}
|
|
internal.EndDatastoreSegment(internal.EndDatastoreParams{
|
|
Tracer: &txn.tracer,
|
|
Start: s.StartTime.start,
|
|
Now: time.Now(),
|
|
Product: string(s.Product),
|
|
Collection: s.Collection,
|
|
Operation: s.Operation,
|
|
ParameterizedQuery: s.ParameterizedQuery,
|
|
QueryParameters: s.QueryParameters,
|
|
Host: s.Host,
|
|
PortPathOrID: s.PortPathOrID,
|
|
Database: s.DatabaseName,
|
|
})
|
|
}
|
|
|
|
func externalSegmentURL(s ExternalSegment) *url.URL {
|
|
if "" != s.URL {
|
|
u, _ := url.Parse(s.URL)
|
|
return u
|
|
}
|
|
r := s.Request
|
|
if nil != s.Response && nil != s.Response.Request {
|
|
r = s.Response.Request
|
|
}
|
|
if r != nil {
|
|
return r.URL
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func endExternal(s ExternalSegment) {
|
|
txn := s.StartTime.txn
|
|
if nil == txn {
|
|
return
|
|
}
|
|
txn.Lock()
|
|
defer txn.Unlock()
|
|
|
|
if txn.finished {
|
|
return
|
|
}
|
|
internal.EndExternalSegment(&txn.tracer, s.StartTime.start, time.Now(), externalSegmentURL(s))
|
|
}
|