github.com/lulzWill/go-agent@v2.1.2+incompatible/internal/slow_queries.go (about)

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