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 }