diff --git a/class.go b/class.go index 7fe255a..3557b76 100644 --- a/class.go +++ b/class.go @@ -21,9 +21,11 @@ type Class struct { UniqueQueries uint // unique number of queries in class Example *Example `json:",omitempty"` // sample query with max Query_time // -- - outliers uint64 - lastDb string - sample bool + outliers uint64 + lastDb string + sample bool + maxQueryTime float64 + MaxQueryCommentMetadata map[string]string } // A Example is a real query and its database, timestamp, and Query_time. @@ -40,12 +42,14 @@ type Example struct { // If sample is true, the query with the greatest Query_time is saved. func NewClass(id, fingerprint string, sample bool) *Class { return &Class{ - Id: id, - Fingerprint: fingerprint, - Metrics: NewMetrics(), - TotalQueries: 0, - Example: &Example{}, - sample: sample, + Id: id, + Fingerprint: fingerprint, + Metrics: NewMetrics(), + TotalQueries: 0, + Example: &Example{}, + sample: sample, + maxQueryTime: 0, + MaxQueryCommentMetadata: map[string]string{}, } } @@ -82,6 +86,13 @@ func (c *Class) AddEvent(e Event, outlier bool) { } } } + + if queryTime, ok := e.TimeMetrics["Query_time"]; ok { + if queryTime > c.maxQueryTime { + c.MaxQueryCommentMetadata = e.CommentMetadata + } + } + } // Finalize calculates all metric statistics. Call this function when done diff --git a/event.go b/event.go index 0957775..04e3010 100644 --- a/event.go +++ b/event.go @@ -12,18 +12,19 @@ package slowlog // event is expected to define the query and Query_time metric. Other metrics // and metadata vary according to MySQL version, distro, and configuration. type Event struct { - Offset uint64 // byte offset in file at which event starts - Ts string // raw timestamp of event - Admin bool // true if Query is admin command - Query string // SQL query or admin command - User string - Host string - Db string - TimeMetrics map[string]float64 // *_time and *_wait metrics - NumberMetrics map[string]uint64 // most metrics - BoolMetrics map[string]bool // yes/no metrics - RateType string // Percona Server rate limit type - RateLimit uint // Percona Server rate limit value + Offset uint64 // byte offset in file at which event starts + Ts string // raw timestamp of event + Admin bool // true if Query is admin command + Query string // SQL query or admin command + User string + Host string + Db string + TimeMetrics map[string]float64 // *_time and *_wait metrics + NumberMetrics map[string]uint64 // most metrics + BoolMetrics map[string]bool // yes/no metrics + RateType string // Percona Server rate limit type + RateLimit uint // Percona Server rate limit value + CommentMetadata map[string]string } // NewEvent returns a new Event with initialized metric maps. diff --git a/parser.go b/parser.go index fc7bedd..021211b 100644 --- a/parser.go +++ b/parser.go @@ -9,6 +9,7 @@ package slowlog import ( "bufio" + "bytes" "errors" "fmt" "io" @@ -396,6 +397,92 @@ func (p *FileParser) parseAdmin(line string) { } } +// Tested here: https://play.golang.org/p/u7XZnxu2LLL +func parseComments(queryWithComments string) map[string]string { + startIndex := strings.Index(queryWithComments, "/*") + if startIndex != -1 { + endIndex := strings.LastIndex(queryWithComments, "*/") + sqlComments := queryWithComments[startIndex:endIndex] + return parseKeyValuePairToMap(sqlComments) + } + return map[string]string{} +} + +// Copied verbatim from https://gist.github.com/alexisvisco/4b846978c9346e4eaf618bb632c0693a +func parseKeyValuePairToMap(msg string) map[string]string { + + type kv struct { + key, val string + } + + var pair *kv = nil + pairs := make(map[string]string) + buf := bytes.NewBuffer([]byte{}) + + var ( + escape = false + garbage = false + quoted = false + ) + + completePair := func(buffer *bytes.Buffer, pair *kv) kv { + if pair != nil { + return kv{pair.key, buffer.String()} + } else { + return kv{buffer.String(), ""} + } + } + + for _, c := range msg { + if !quoted && c == ' ' { + if buf.Len() != 0 { + if !garbage { + p := completePair(buf, pair) + pairs[p.key] = p.val + pair = nil + } + buf.Reset() + } + garbage = false + } else if !quoted && c == '=' { + if buf.Len() != 0 { + pair = &kv{key: buf.String(), val: ""} + buf.Reset() + } else { + garbage = true + } + } else if quoted && c == '\\' { + escape = true + } else if c == '"' { + if escape { + buf.WriteRune(c) + escape = false + } else { + quoted = !quoted + } + } else { + if escape { + buf.WriteRune('\\') + escape = false + } + buf.WriteRune(c) + } + } + + if !garbage { + p := completePair(buf, pair) + pairs[p.key] = p.val + } + + // Remove any key value pair where either of them is a blank string + for key, value := range pairs { + if key == "" || value == "" { + delete(pairs, key) + } + } + return pairs +} + func (p *FileParser) sendEvent(inHeader bool, inQuery bool) { if Debug { log.Println("send event") @@ -421,6 +508,7 @@ func (p *FileParser) sendEvent(inHeader bool, inQuery bool) { // Clean up the event. p.event.Db = strings.TrimSuffix(p.event.Db, ";\n") p.event.Query = strings.TrimSuffix(p.event.Query, ";") + p.event.CommentMetadata = parseComments(p.event.Query) // Send the event. This will block. select {