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 }