414 lines
10 KiB
Go
414 lines
10 KiB
Go
package internal
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/newrelic/go-agent/internal/sysinfo"
|
|
)
|
|
|
|
type segmentStamp uint64
|
|
|
|
type segmentTime struct {
|
|
Stamp segmentStamp
|
|
Time time.Time
|
|
}
|
|
|
|
// SegmentStartTime is embedded into the top level segments (rather than
|
|
// segmentTime) to minimize the structure sizes to minimize allocations.
|
|
type SegmentStartTime struct {
|
|
Stamp segmentStamp
|
|
Depth int
|
|
}
|
|
|
|
type segmentFrame struct {
|
|
segmentTime
|
|
children time.Duration
|
|
}
|
|
|
|
type segmentEnd struct {
|
|
valid bool
|
|
start segmentTime
|
|
stop segmentTime
|
|
duration time.Duration
|
|
exclusive time.Duration
|
|
}
|
|
|
|
// Tracer tracks segments.
|
|
type Tracer struct {
|
|
finishedChildren time.Duration
|
|
stamp segmentStamp
|
|
currentDepth int
|
|
stack []segmentFrame
|
|
|
|
customSegments map[string]*metricData
|
|
datastoreSegments map[DatastoreMetricKey]*metricData
|
|
externalSegments map[externalMetricKey]*metricData
|
|
|
|
DatastoreExternalTotals
|
|
|
|
TxnTrace
|
|
|
|
SlowQueriesEnabled bool
|
|
SlowQueryThreshold time.Duration
|
|
SlowQueries *slowQueries
|
|
}
|
|
|
|
const (
|
|
startingStackDepthAlloc = 128
|
|
datastoreProductUnknown = "Unknown"
|
|
datastoreOperationUnknown = "other"
|
|
)
|
|
|
|
func (t *Tracer) time(now time.Time) segmentTime {
|
|
// Update the stamp before using it so that a 0 stamp can be special.
|
|
t.stamp++
|
|
return segmentTime{
|
|
Time: now,
|
|
Stamp: t.stamp,
|
|
}
|
|
}
|
|
|
|
// TracerRootChildren is used to calculate a transaction's exclusive duration.
|
|
func TracerRootChildren(t *Tracer) time.Duration {
|
|
var lostChildren time.Duration
|
|
for i := 0; i < t.currentDepth; i++ {
|
|
lostChildren += t.stack[i].children
|
|
}
|
|
return t.finishedChildren + lostChildren
|
|
}
|
|
|
|
// StartSegment begins a segment.
|
|
func StartSegment(t *Tracer, now time.Time) SegmentStartTime {
|
|
if nil == t.stack {
|
|
t.stack = make([]segmentFrame, startingStackDepthAlloc)
|
|
}
|
|
if cap(t.stack) == t.currentDepth {
|
|
newLimit := 2 * t.currentDepth
|
|
newStack := make([]segmentFrame, newLimit)
|
|
copy(newStack, t.stack)
|
|
t.stack = newStack
|
|
}
|
|
|
|
tm := t.time(now)
|
|
|
|
depth := t.currentDepth
|
|
t.currentDepth++
|
|
t.stack[depth].children = 0
|
|
t.stack[depth].segmentTime = tm
|
|
|
|
return SegmentStartTime{
|
|
Stamp: tm.Stamp,
|
|
Depth: depth,
|
|
}
|
|
}
|
|
|
|
func endSegment(t *Tracer, start SegmentStartTime, now time.Time) segmentEnd {
|
|
var s segmentEnd
|
|
if 0 == start.Stamp {
|
|
return s
|
|
}
|
|
if start.Depth >= t.currentDepth {
|
|
return s
|
|
}
|
|
if start.Depth < 0 {
|
|
return s
|
|
}
|
|
if start.Stamp != t.stack[start.Depth].Stamp {
|
|
return s
|
|
}
|
|
|
|
var children time.Duration
|
|
for i := start.Depth; i < t.currentDepth; i++ {
|
|
children += t.stack[i].children
|
|
}
|
|
s.valid = true
|
|
s.stop = t.time(now)
|
|
s.start = t.stack[start.Depth].segmentTime
|
|
if s.stop.Time.After(s.start.Time) {
|
|
s.duration = s.stop.Time.Sub(s.start.Time)
|
|
}
|
|
if s.duration > children {
|
|
s.exclusive = s.duration - children
|
|
}
|
|
|
|
// Note that we expect (depth == (t.currentDepth - 1)). However, if
|
|
// (depth < (t.currentDepth - 1)), that's ok: could be a panic popped
|
|
// some stack frames (and the consumer was not using defer).
|
|
t.currentDepth = start.Depth
|
|
|
|
if 0 == t.currentDepth {
|
|
t.finishedChildren += s.duration
|
|
} else {
|
|
t.stack[t.currentDepth-1].children += s.duration
|
|
}
|
|
return s
|
|
}
|
|
|
|
// EndBasicSegment ends a basic segment.
|
|
func EndBasicSegment(t *Tracer, start SegmentStartTime, now time.Time, name string) {
|
|
end := endSegment(t, start, now)
|
|
if !end.valid {
|
|
return
|
|
}
|
|
if nil == t.customSegments {
|
|
t.customSegments = make(map[string]*metricData)
|
|
}
|
|
m := metricDataFromDuration(end.duration, end.exclusive)
|
|
if data, ok := t.customSegments[name]; ok {
|
|
data.aggregate(m)
|
|
} else {
|
|
// Use `new` in place of &m so that m is not
|
|
// automatically moved to the heap.
|
|
cpy := new(metricData)
|
|
*cpy = m
|
|
t.customSegments[name] = cpy
|
|
}
|
|
|
|
if t.TxnTrace.considerNode(end) {
|
|
t.TxnTrace.witnessNode(end, customSegmentMetric(name), nil)
|
|
}
|
|
}
|
|
|
|
// EndExternalSegment ends an external segment.
|
|
func EndExternalSegment(t *Tracer, start SegmentStartTime, now time.Time, u *url.URL) {
|
|
end := endSegment(t, start, now)
|
|
if !end.valid {
|
|
return
|
|
}
|
|
host := HostFromURL(u)
|
|
if "" == host {
|
|
host = "unknown"
|
|
}
|
|
key := externalMetricKey{
|
|
Host: host,
|
|
ExternalCrossProcessID: "",
|
|
ExternalTransactionName: "",
|
|
}
|
|
if nil == t.externalSegments {
|
|
t.externalSegments = make(map[externalMetricKey]*metricData)
|
|
}
|
|
t.externalCallCount++
|
|
t.externalDuration += end.duration
|
|
m := metricDataFromDuration(end.duration, end.exclusive)
|
|
if data, ok := t.externalSegments[key]; ok {
|
|
data.aggregate(m)
|
|
} else {
|
|
// Use `new` in place of &m so that m is not
|
|
// automatically moved to the heap.
|
|
cpy := new(metricData)
|
|
*cpy = m
|
|
t.externalSegments[key] = cpy
|
|
}
|
|
|
|
if t.TxnTrace.considerNode(end) {
|
|
t.TxnTrace.witnessNode(end, externalHostMetric(key), &traceNodeParams{
|
|
CleanURL: SafeURL(u),
|
|
})
|
|
}
|
|
}
|
|
|
|
// EndDatastoreParams contains the parameters for EndDatastoreSegment.
|
|
type EndDatastoreParams struct {
|
|
Tracer *Tracer
|
|
Start SegmentStartTime
|
|
Now time.Time
|
|
Product string
|
|
Collection string
|
|
Operation string
|
|
ParameterizedQuery string
|
|
QueryParameters map[string]interface{}
|
|
Host string
|
|
PortPathOrID string
|
|
Database string
|
|
}
|
|
|
|
const (
|
|
unknownDatastoreHost = "unknown"
|
|
unknownDatastorePortPathOrID = "unknown"
|
|
)
|
|
|
|
var (
|
|
// ThisHost is the system hostname.
|
|
ThisHost = func() string {
|
|
if h, err := sysinfo.Hostname(); nil == err {
|
|
return h
|
|
}
|
|
return unknownDatastoreHost
|
|
}()
|
|
hostsToReplace = map[string]struct{}{
|
|
"localhost": struct{}{},
|
|
"127.0.0.1": struct{}{},
|
|
"0.0.0.0": struct{}{},
|
|
"0:0:0:0:0:0:0:1": struct{}{},
|
|
"::1": struct{}{},
|
|
"0:0:0:0:0:0:0:0": struct{}{},
|
|
"::": struct{}{},
|
|
}
|
|
)
|
|
|
|
func (t Tracer) slowQueryWorthy(d time.Duration) bool {
|
|
return t.SlowQueriesEnabled && (d >= t.SlowQueryThreshold)
|
|
}
|
|
|
|
// EndDatastoreSegment ends a datastore segment.
|
|
func EndDatastoreSegment(p EndDatastoreParams) {
|
|
end := endSegment(p.Tracer, p.Start, p.Now)
|
|
if !end.valid {
|
|
return
|
|
}
|
|
if p.Operation == "" {
|
|
p.Operation = datastoreOperationUnknown
|
|
}
|
|
if p.Product == "" {
|
|
p.Product = datastoreProductUnknown
|
|
}
|
|
if p.Host == "" && p.PortPathOrID != "" {
|
|
p.Host = unknownDatastoreHost
|
|
}
|
|
if p.PortPathOrID == "" && p.Host != "" {
|
|
p.PortPathOrID = unknownDatastorePortPathOrID
|
|
}
|
|
if _, ok := hostsToReplace[p.Host]; ok {
|
|
p.Host = ThisHost
|
|
}
|
|
|
|
// We still want to create a slowQuery if the consumer has not provided
|
|
// a Query string since the stack trace has value.
|
|
if p.ParameterizedQuery == "" {
|
|
collection := p.Collection
|
|
if "" == collection {
|
|
collection = "unknown"
|
|
}
|
|
p.ParameterizedQuery = fmt.Sprintf(`'%s' on '%s' using '%s'`,
|
|
p.Operation, collection, p.Product)
|
|
}
|
|
|
|
key := DatastoreMetricKey{
|
|
Product: p.Product,
|
|
Collection: p.Collection,
|
|
Operation: p.Operation,
|
|
Host: p.Host,
|
|
PortPathOrID: p.PortPathOrID,
|
|
}
|
|
if nil == p.Tracer.datastoreSegments {
|
|
p.Tracer.datastoreSegments = make(map[DatastoreMetricKey]*metricData)
|
|
}
|
|
p.Tracer.datastoreCallCount++
|
|
p.Tracer.datastoreDuration += end.duration
|
|
m := metricDataFromDuration(end.duration, end.exclusive)
|
|
if data, ok := p.Tracer.datastoreSegments[key]; ok {
|
|
data.aggregate(m)
|
|
} else {
|
|
// Use `new` in place of &m so that m is not
|
|
// automatically moved to the heap.
|
|
cpy := new(metricData)
|
|
*cpy = m
|
|
p.Tracer.datastoreSegments[key] = cpy
|
|
}
|
|
|
|
scopedMetric := datastoreScopedMetric(key)
|
|
queryParams := vetQueryParameters(p.QueryParameters)
|
|
|
|
if p.Tracer.TxnTrace.considerNode(end) {
|
|
p.Tracer.TxnTrace.witnessNode(end, scopedMetric, &traceNodeParams{
|
|
Host: p.Host,
|
|
PortPathOrID: p.PortPathOrID,
|
|
Database: p.Database,
|
|
Query: p.ParameterizedQuery,
|
|
queryParameters: queryParams,
|
|
})
|
|
}
|
|
|
|
if p.Tracer.slowQueryWorthy(end.duration) {
|
|
if nil == p.Tracer.SlowQueries {
|
|
p.Tracer.SlowQueries = newSlowQueries(maxTxnSlowQueries)
|
|
}
|
|
// Frames to skip:
|
|
// this function
|
|
// endDatastore
|
|
// DatastoreSegment.End
|
|
skipFrames := 3
|
|
p.Tracer.SlowQueries.observeInstance(slowQueryInstance{
|
|
Duration: end.duration,
|
|
DatastoreMetric: scopedMetric,
|
|
ParameterizedQuery: p.ParameterizedQuery,
|
|
QueryParameters: queryParams,
|
|
Host: p.Host,
|
|
PortPathOrID: p.PortPathOrID,
|
|
DatabaseName: p.Database,
|
|
StackTrace: GetStackTrace(skipFrames),
|
|
})
|
|
}
|
|
}
|
|
|
|
// MergeBreakdownMetrics creates segment metrics.
|
|
func MergeBreakdownMetrics(t *Tracer, metrics *metricTable, scope string, isWeb bool) {
|
|
// Custom Segment Metrics
|
|
for key, data := range t.customSegments {
|
|
name := customSegmentMetric(key)
|
|
// Unscoped
|
|
metrics.add(name, "", *data, unforced)
|
|
// Scoped
|
|
metrics.add(name, scope, *data, unforced)
|
|
}
|
|
|
|
// External Segment Metrics
|
|
for key, data := range t.externalSegments {
|
|
metrics.add(externalAll, "", *data, forced)
|
|
if isWeb {
|
|
metrics.add(externalWeb, "", *data, forced)
|
|
} else {
|
|
metrics.add(externalOther, "", *data, forced)
|
|
}
|
|
hostMetric := externalHostMetric(key)
|
|
metrics.add(hostMetric, "", *data, unforced)
|
|
if "" != key.ExternalCrossProcessID && "" != key.ExternalTransactionName {
|
|
txnMetric := externalTransactionMetric(key)
|
|
|
|
// Unscoped CAT metrics
|
|
metrics.add(externalAppMetric(key), "", *data, unforced)
|
|
metrics.add(txnMetric, "", *data, unforced)
|
|
|
|
// Scoped External Metric
|
|
metrics.add(txnMetric, scope, *data, unforced)
|
|
} else {
|
|
// Scoped External Metric
|
|
metrics.add(hostMetric, scope, *data, unforced)
|
|
}
|
|
}
|
|
|
|
// Datastore Segment Metrics
|
|
for key, data := range t.datastoreSegments {
|
|
metrics.add(datastoreAll, "", *data, forced)
|
|
|
|
product := datastoreProductMetric(key)
|
|
metrics.add(product.All, "", *data, forced)
|
|
if isWeb {
|
|
metrics.add(datastoreWeb, "", *data, forced)
|
|
metrics.add(product.Web, "", *data, forced)
|
|
} else {
|
|
metrics.add(datastoreOther, "", *data, forced)
|
|
metrics.add(product.Other, "", *data, forced)
|
|
}
|
|
|
|
if key.Host != "" && key.PortPathOrID != "" {
|
|
instance := datastoreInstanceMetric(key)
|
|
metrics.add(instance, "", *data, unforced)
|
|
}
|
|
|
|
operation := datastoreOperationMetric(key)
|
|
metrics.add(operation, "", *data, unforced)
|
|
|
|
if "" != key.Collection {
|
|
statement := datastoreStatementMetric(key)
|
|
|
|
metrics.add(statement, "", *data, unforced)
|
|
metrics.add(statement, scope, *data, unforced)
|
|
} else {
|
|
metrics.add(operation, scope, *data, unforced)
|
|
}
|
|
}
|
|
}
|