github.com/newrelic/go-agent@v3.26.0+incompatible/internal/slow_queries.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  	"container/heap"
     9  	"hash/fnv"
    10  	"time"
    11  
    12  	"github.com/newrelic/go-agent/internal/jsonx"
    13  )
    14  
    15  type queryParameters map[string]interface{}
    16  
    17  func vetQueryParameters(params map[string]interface{}) (queryParameters, error) {
    18  	if nil == params {
    19  		return nil, nil
    20  	}
    21  	// Copying the parameters into a new map is safer than modifying the map
    22  	// from the customer.
    23  	vetted := make(map[string]interface{})
    24  	var retErr error
    25  	for key, val := range params {
    26  		val, err := ValidateUserAttribute(key, val)
    27  		if nil != err {
    28  			retErr = err
    29  			continue
    30  		}
    31  		vetted[key] = val
    32  	}
    33  	return queryParameters(vetted), retErr
    34  }
    35  
    36  func (q queryParameters) WriteJSON(buf *bytes.Buffer) {
    37  	buf.WriteByte('{')
    38  	w := jsonFieldsWriter{buf: buf}
    39  	for key, val := range q {
    40  		writeAttributeValueJSON(&w, key, val)
    41  	}
    42  	buf.WriteByte('}')
    43  }
    44  
    45  // https://source.datanerd.us/agents/agent-specs/blob/master/Slow-SQLs-LEGACY.md
    46  
    47  // slowQueryInstance represents a single datastore call.
    48  type slowQueryInstance struct {
    49  	// Fields populated right after the datastore segment finishes:
    50  
    51  	Duration           time.Duration
    52  	DatastoreMetric    string
    53  	ParameterizedQuery string
    54  	QueryParameters    queryParameters
    55  	Host               string
    56  	PortPathOrID       string
    57  	DatabaseName       string
    58  	StackTrace         StackTrace
    59  
    60  	TxnEvent
    61  }
    62  
    63  // Aggregation is performed to avoid reporting multiple slow queries with same
    64  // query string.  Since some datastore segments may be below the slow query
    65  // threshold, the aggregation fields Count, Total, and Min should be taken with
    66  // a grain of salt.
    67  type slowQuery struct {
    68  	Count int32         // number of times the query has been observed
    69  	Total time.Duration // cummulative duration
    70  	Min   time.Duration // minimum observed duration
    71  
    72  	// When Count > 1, slowQueryInstance contains values from the slowest
    73  	// observation.
    74  	slowQueryInstance
    75  }
    76  
    77  type slowQueries struct {
    78  	priorityQueue []*slowQuery
    79  	// lookup maps query strings to indices in the priorityQueue
    80  	lookup map[string]int
    81  }
    82  
    83  func (slows *slowQueries) Len() int {
    84  	return len(slows.priorityQueue)
    85  }
    86  func (slows *slowQueries) Less(i, j int) bool {
    87  	pq := slows.priorityQueue
    88  	return pq[i].Duration < pq[j].Duration
    89  }
    90  func (slows *slowQueries) Swap(i, j int) {
    91  	pq := slows.priorityQueue
    92  	si := pq[i]
    93  	sj := pq[j]
    94  	pq[i], pq[j] = pq[j], pq[i]
    95  	slows.lookup[si.ParameterizedQuery] = j
    96  	slows.lookup[sj.ParameterizedQuery] = i
    97  }
    98  
    99  // Push and Pop are unused: only heap.Init and heap.Fix are used.
   100  func (slows *slowQueries) Push(x interface{}) {}
   101  func (slows *slowQueries) Pop() interface{}   { return nil }
   102  
   103  func newSlowQueries(max int) *slowQueries {
   104  	return &slowQueries{
   105  		lookup:        make(map[string]int, max),
   106  		priorityQueue: make([]*slowQuery, 0, max),
   107  	}
   108  }
   109  
   110  // Merge is used to merge slow queries from the transaction into the harvest.
   111  func (slows *slowQueries) Merge(other *slowQueries, txnEvent TxnEvent) {
   112  	for _, s := range other.priorityQueue {
   113  		cp := *s
   114  		cp.TxnEvent = txnEvent
   115  		slows.observe(cp)
   116  	}
   117  }
   118  
   119  // merge aggregates the observations from two slow queries with the same Query.
   120  func (slow *slowQuery) merge(other slowQuery) {
   121  	slow.Count += other.Count
   122  	slow.Total += other.Total
   123  
   124  	if other.Min < slow.Min {
   125  		slow.Min = other.Min
   126  	}
   127  	if other.Duration > slow.Duration {
   128  		slow.slowQueryInstance = other.slowQueryInstance
   129  	}
   130  }
   131  
   132  func (slows *slowQueries) observeInstance(slow slowQueryInstance) {
   133  	slows.observe(slowQuery{
   134  		Count:             1,
   135  		Total:             slow.Duration,
   136  		Min:               slow.Duration,
   137  		slowQueryInstance: slow,
   138  	})
   139  }
   140  
   141  func (slows *slowQueries) insertAtIndex(slow slowQuery, idx int) {
   142  	cpy := new(slowQuery)
   143  	*cpy = slow
   144  	slows.priorityQueue[idx] = cpy
   145  	slows.lookup[slow.ParameterizedQuery] = idx
   146  	heap.Fix(slows, idx)
   147  }
   148  
   149  func (slows *slowQueries) observe(slow slowQuery) {
   150  	// Has the query has previously been observed?
   151  	if idx, ok := slows.lookup[slow.ParameterizedQuery]; ok {
   152  		slows.priorityQueue[idx].merge(slow)
   153  		heap.Fix(slows, idx)
   154  		return
   155  	}
   156  	// Has the collection reached max capacity?
   157  	if len(slows.priorityQueue) < cap(slows.priorityQueue) {
   158  		idx := len(slows.priorityQueue)
   159  		slows.priorityQueue = slows.priorityQueue[0 : idx+1]
   160  		slows.insertAtIndex(slow, idx)
   161  		return
   162  	}
   163  	// Is this query slower than the existing fastest?
   164  	fastest := slows.priorityQueue[0]
   165  	if slow.Duration > fastest.Duration {
   166  		delete(slows.lookup, fastest.ParameterizedQuery)
   167  		slows.insertAtIndex(slow, 0)
   168  		return
   169  	}
   170  }
   171  
   172  // The third element of the slow query JSON should be a hash of the query
   173  // string.  This hash may be used by backend services to aggregate queries which
   174  // have the have the same query string.  It is unknown if this actually used.
   175  func makeSlowQueryID(query string) uint32 {
   176  	h := fnv.New32a()
   177  	h.Write([]byte(query))
   178  	return h.Sum32()
   179  }
   180  
   181  func (slow *slowQuery) WriteJSON(buf *bytes.Buffer) {
   182  	buf.WriteByte('[')
   183  	jsonx.AppendString(buf, slow.TxnEvent.FinalName)
   184  	buf.WriteByte(',')
   185  	// Include request.uri if it is included in any destination.
   186  	// TODO: Change this to the transaction trace segment destination
   187  	// once transaction trace segment attribute configuration has been
   188  	// added.
   189  	uri, _ := slow.TxnEvent.Attrs.GetAgentValue(attributeRequestURI, DestAll)
   190  	jsonx.AppendString(buf, uri)
   191  	buf.WriteByte(',')
   192  	jsonx.AppendInt(buf, int64(makeSlowQueryID(slow.ParameterizedQuery)))
   193  	buf.WriteByte(',')
   194  	jsonx.AppendString(buf, slow.ParameterizedQuery)
   195  	buf.WriteByte(',')
   196  	jsonx.AppendString(buf, slow.DatastoreMetric)
   197  	buf.WriteByte(',')
   198  	jsonx.AppendInt(buf, int64(slow.Count))
   199  	buf.WriteByte(',')
   200  	jsonx.AppendFloat(buf, slow.Total.Seconds()*1000.0)
   201  	buf.WriteByte(',')
   202  	jsonx.AppendFloat(buf, slow.Min.Seconds()*1000.0)
   203  	buf.WriteByte(',')
   204  	jsonx.AppendFloat(buf, slow.Duration.Seconds()*1000.0)
   205  	buf.WriteByte(',')
   206  	w := jsonFieldsWriter{buf: buf}
   207  	buf.WriteByte('{')
   208  	if "" != slow.Host {
   209  		w.stringField("host", slow.Host)
   210  	}
   211  	if "" != slow.PortPathOrID {
   212  		w.stringField("port_path_or_id", slow.PortPathOrID)
   213  	}
   214  	if "" != slow.DatabaseName {
   215  		w.stringField("database_name", slow.DatabaseName)
   216  	}
   217  	if nil != slow.StackTrace {
   218  		w.writerField("backtrace", slow.StackTrace)
   219  	}
   220  	if nil != slow.QueryParameters {
   221  		w.writerField("query_parameters", slow.QueryParameters)
   222  	}
   223  
   224  	sharedBetterCATIntrinsics(&slow.TxnEvent, &w)
   225  
   226  	buf.WriteByte('}')
   227  	buf.WriteByte(']')
   228  }
   229  
   230  // WriteJSON marshals the collection of slow queries into JSON according to the
   231  // schema expected by the collector.
   232  //
   233  // Note: This JSON does not contain the agentRunID.  This is for unknown
   234  // historical reasons. Since the agentRunID is included in the url,
   235  // its use in the other commands' JSON is redundant (although required).
   236  func (slows *slowQueries) WriteJSON(buf *bytes.Buffer) {
   237  	buf.WriteByte('[')
   238  	buf.WriteByte('[')
   239  	for idx, s := range slows.priorityQueue {
   240  		if idx > 0 {
   241  			buf.WriteByte(',')
   242  		}
   243  		s.WriteJSON(buf)
   244  	}
   245  	buf.WriteByte(']')
   246  	buf.WriteByte(']')
   247  }
   248  
   249  func (slows *slowQueries) Data(agentRunID string, harvestStart time.Time) ([]byte, error) {
   250  	if 0 == len(slows.priorityQueue) {
   251  		return nil, nil
   252  	}
   253  	estimate := 1024 * len(slows.priorityQueue)
   254  	buf := bytes.NewBuffer(make([]byte, 0, estimate))
   255  	slows.WriteJSON(buf)
   256  	return buf.Bytes(), nil
   257  }
   258  
   259  func (slows *slowQueries) MergeIntoHarvest(newHarvest *Harvest) {
   260  }
   261  
   262  func (slows *slowQueries) EndpointMethod() string {
   263  	return cmdSlowSQLs
   264  }