Skip to content

Commit 070cf62

Browse files
Initial code commit, copied and modified from several percona/go-mysql pkgs @ 2a6037d
1 parent cea3e7d commit 070cf62

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+4957
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.swp
2+
*.out

Godeps/Godeps.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Godeps/Readme

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Slow Log Parsing and Aggregation
2+
3+
[![GoDoc](https://godoc.org/github.com/go-mysql/slowlog?status.svg)](https://godoc.org/github.com/go-mysql/slowlog)
4+
5+
This package provides functions for working with the MySQL slow log.
6+
7+
## Acknowledgement
8+
9+
This code was originally copied from [percona/go-mysql](https://github.com/percona/go-mysql) @ `2a6037d7d809b18ebd6d735b397f2321879af611`. See that project for original contributors and copyright.
10+
11+
This project is a fork to continue development of percona/go-mysql as separate packages. GitHub only allows forking a project once, which is why the code has been copied.

aggregator.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
Copyright 2017 Daniel Nichter
3+
Copyright 2014-2016 Percona LLC and/or its affiliates
4+
*/
5+
6+
package slowlog
7+
8+
import (
9+
"time"
10+
)
11+
12+
// A Result contains a global class and per-ID classes with finalized metric
13+
// statistics. The classes are keyed on class ID.
14+
type Result struct {
15+
Global *Class // all classes
16+
Class map[string]*Class // keyed on class ID
17+
RateLimit uint
18+
Error string
19+
}
20+
21+
// An Aggregator groups events by class ID. When there are no more events,
22+
// a call to Finalize computes all metric statistics and returns a Result.
23+
type Aggregator struct {
24+
samples bool
25+
utcOffset time.Duration
26+
outlierTime float64
27+
// --
28+
global *Class
29+
classes map[string]*Class
30+
rateLimit uint
31+
}
32+
33+
// NewAggregator returns a new Aggregator.
34+
func NewAggregator(samples bool, utcOffset time.Duration, outlierTime float64) *Aggregator {
35+
a := &Aggregator{
36+
samples: samples,
37+
utcOffset: utcOffset,
38+
outlierTime: outlierTime,
39+
// --
40+
global: NewClass("", "", false),
41+
classes: map[string]*Class{},
42+
}
43+
return a
44+
}
45+
46+
// AddEvent adds the event to the aggregator, automatically creating new classes
47+
// as needed.
48+
func (a *Aggregator) AddEvent(event Event, id, fingerprint string) {
49+
if a.rateLimit != event.RateLimit {
50+
a.rateLimit = event.RateLimit
51+
}
52+
53+
outlier := false
54+
if a.outlierTime > 0 && event.TimeMetrics["Query_time"] > a.outlierTime {
55+
outlier = true
56+
}
57+
58+
a.global.AddEvent(event, outlier)
59+
60+
class, ok := a.classes[id]
61+
if !ok {
62+
class = NewClass(id, fingerprint, a.samples)
63+
a.classes[id] = class
64+
}
65+
class.AddEvent(event, outlier)
66+
}
67+
68+
// Finalize calculates all metric statistics and returns a Result.
69+
// Call this function when done adding events to the aggregator.
70+
func (a *Aggregator) Finalize() Result {
71+
a.global.Finalize(a.rateLimit)
72+
a.global.UniqueQueries = uint(len(a.classes))
73+
for _, class := range a.classes {
74+
class.Finalize(a.rateLimit)
75+
class.UniqueQueries = 1
76+
if class.Example != nil && class.Example.Ts != "" {
77+
if t, err := time.Parse("060102 15:04:05", class.Example.Ts); err != nil {
78+
class.Example.Ts = ""
79+
} else {
80+
class.Example.Ts = t.Add(a.utcOffset).Format("2006-01-02 15:04:05")
81+
}
82+
}
83+
}
84+
return Result{
85+
Global: a.global,
86+
Class: a.classes,
87+
RateLimit: a.rateLimit,
88+
}
89+
}

aggregator_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
Copyright 2017 Daniel Nichter
3+
Copyright 2014-2016 Percona LLC and/or its affiliates
4+
*/
5+
6+
package slowlog_test
7+
8+
import (
9+
"encoding/json"
10+
"io/ioutil"
11+
"os"
12+
"path"
13+
"testing"
14+
"time"
15+
16+
"github.com/daniel-nichter/deep-equal"
17+
"github.com/go-mysql/query"
18+
"github.com/go-mysql/slowlog"
19+
)
20+
21+
var examples = true
22+
23+
func aggregateSlowLog(t *testing.T, input, output string, utcOffset time.Duration) (got slowlog.Result, expect slowlog.Result) {
24+
bytes, err := ioutil.ReadFile(path.Join("test", "results", output))
25+
if err != nil {
26+
t.Fatal(err)
27+
}
28+
expect = slowlog.Result{}
29+
if err := json.Unmarshal(bytes, &expect); err != nil {
30+
t.Fatal(err)
31+
}
32+
33+
file, err := os.Open(path.Join("test", "slow-logs", input))
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
p := slowlog.NewFileParser(file)
38+
if err != nil {
39+
t.Fatal(err)
40+
}
41+
if err := p.Start(slowlog.Options{}); err != nil {
42+
t.Fatal(err)
43+
}
44+
defer p.Stop()
45+
46+
a := slowlog.NewAggregator(examples, utcOffset, 10)
47+
for e := range p.Events() {
48+
f := query.Fingerprint(e.Query)
49+
id := query.Id(f)
50+
a.AddEvent(e, id, f)
51+
}
52+
got = a.Finalize()
53+
return got, expect
54+
}
55+
56+
func zeroPercentiles(r *slowlog.Result) {
57+
for _, metrics := range r.Global.Metrics.TimeMetrics {
58+
metrics.Med = 0
59+
metrics.P95 = 0
60+
}
61+
for _, metrics := range r.Global.Metrics.NumberMetrics {
62+
metrics.Med = 0
63+
metrics.P95 = 0
64+
}
65+
for _, class := range r.Class {
66+
for _, metrics := range class.Metrics.TimeMetrics {
67+
metrics.Med = 0
68+
metrics.P95 = 0
69+
}
70+
for _, metrics := range class.Metrics.NumberMetrics {
71+
metrics.Med = 0
72+
metrics.P95 = 0
73+
}
74+
}
75+
}
76+
77+
// --------------------------------------------------------------------------
78+
79+
func TestSlow001(t *testing.T) {
80+
got, expect := aggregateSlowLog(t, "slow001.log", "slow001.json", 0)
81+
if diff, _ := deep.Equal(got, expect); diff != nil {
82+
dump(got)
83+
t.Error(diff)
84+
}
85+
}
86+
87+
func TestSlow001WithTzOffset(t *testing.T) {
88+
got, expect := aggregateSlowLog(t, "slow001.log", "slow001.json", -1*time.Hour)
89+
// Use the same files as TestSlow001NoExamples but with a tz=-1
90+
expect.Class["7F7D57ACDD8A346E"].Example.Ts = "2007-10-15 20:43:52"
91+
expect.Class["3A99CC42AEDCCFCD"].Example.Ts = "2007-10-15 20:45:10"
92+
if diff, _ := deep.Equal(got, expect); diff != nil {
93+
dump(got)
94+
t.Error(diff)
95+
}
96+
}
97+
98+
func TestSlow001NoExamples(t *testing.T) {
99+
examples = false
100+
defer func() { examples = true }()
101+
got, expect := aggregateSlowLog(t, "slow001.log", "slow001-no-examples.json", 0)
102+
if diff, _ := deep.Equal(got, expect); diff != nil {
103+
dump(got)
104+
t.Error(diff)
105+
}
106+
}
107+
108+
// Test p95 and median.
109+
func TestSlow010(t *testing.T) {
110+
got, expect := aggregateSlowLog(t, "slow010.log", "slow010.json", 0)
111+
if diff, _ := deep.Equal(got, expect); diff != nil {
112+
dump(got)
113+
t.Error(diff)
114+
}
115+
}
116+
117+
func TestSlow018(t *testing.T) {
118+
got, expect := aggregateSlowLog(t, "slow018.log", "slow018.json", 0)
119+
if diff, _ := deep.Equal(got, expect); diff != nil {
120+
dump(got)
121+
t.Error(diff)
122+
}
123+
}
124+
125+
// Tests for PCT-1006 & PCT-1085
126+
func TestUseDb(t *testing.T) {
127+
// Test db is not inherited
128+
got, expect := aggregateSlowLog(t, "slow020.log", "slow020.json", 0)
129+
if diff, _ := deep.Equal(got, expect); diff != nil {
130+
dump(got)
131+
t.Error(diff)
132+
}
133+
// Test "use" is not case sensitive
134+
got, expect = aggregateSlowLog(t, "slow021.log", "slow021.json", 0)
135+
if diff, _ := deep.Equal(got, expect); diff != nil {
136+
dump(got)
137+
t.Error(diff)
138+
}
139+
// Test we are parsing db names in backticks
140+
got, expect = aggregateSlowLog(t, "slow022.log", "slow022.json", 0)
141+
if diff, _ := deep.Equal(got, expect); diff != nil {
142+
dump(got)
143+
t.Error(diff)
144+
}
145+
}
146+
147+
func TestOutlierSlow025(t *testing.T) {
148+
got, expect := aggregateSlowLog(t, "slow025.log", "slow025.json", 0)
149+
if diff, _ := deep.Equal(got, expect); diff != nil {
150+
dump(got)
151+
t.Error(diff)
152+
}
153+
}

class.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
Copyright 2017 Daniel Nichter
3+
Copyright 2014-2016 Percona LLC and/or its affiliates
4+
*/
5+
6+
package slowlog
7+
8+
const (
9+
MAX_EXAMPLE_BYTES = 1024 * 10
10+
)
11+
12+
// A Class represents all events with the same fingerprint and class ID.
13+
// This is only enforced by convention, so be careful not to mix events from
14+
// different classes.
15+
type Class struct {
16+
Id string // 32-character hex checksum of fingerprint
17+
Fingerprint string // canonical form of query: values replaced with "?"
18+
Metrics Metrics // statistics for each metric, e.g. max Query_time
19+
TotalQueries uint // total number of queries in class
20+
UniqueQueries uint // unique number of queries in class
21+
Example *Example `json:",omitempty"` // sample query with max Query_time
22+
// --
23+
outliers uint
24+
lastDb string
25+
sample bool
26+
}
27+
28+
// A Example is a real query and its database, timestamp, and Query_time.
29+
// If the query is larger than MAX_EXAMPLE_BYTES, it is truncated and "..."
30+
// is appended.
31+
type Example struct {
32+
QueryTime float64 // Query_time
33+
Db string // Schema: <db> or USE <db>
34+
Query string // truncated to MAX_EXAMPLE_BYTES
35+
Ts string `json:",omitempty"` // in MySQL time zone
36+
}
37+
38+
// NewClass returns a new Class for the class ID and fingerprint.
39+
// If sample is true, the query with the greatest Query_time is saved.
40+
func NewClass(id, fingerprint string, sample bool) *Class {
41+
return &Class{
42+
Id: id,
43+
Fingerprint: fingerprint,
44+
Metrics: NewMetrics(),
45+
TotalQueries: 0,
46+
Example: &Example{},
47+
sample: sample,
48+
}
49+
}
50+
51+
// AddEvent adds an event to the query class.
52+
func (c *Class) AddEvent(e Event, outlier bool) {
53+
if outlier {
54+
c.outliers++
55+
} else {
56+
c.TotalQueries++
57+
}
58+
59+
c.Metrics.AddEvent(e, outlier)
60+
61+
// Save last db seen for this query. This helps ensure the sample query
62+
// has a db.
63+
if e.Db != "" {
64+
c.lastDb = e.Db
65+
}
66+
if c.sample {
67+
if n, ok := e.TimeMetrics["Query_time"]; ok {
68+
if float64(n) > c.Example.QueryTime {
69+
c.Example.QueryTime = float64(n)
70+
if e.Db != "" {
71+
c.Example.Db = e.Db
72+
} else {
73+
c.Example.Db = c.lastDb
74+
}
75+
if len(e.Query) > MAX_EXAMPLE_BYTES {
76+
c.Example.Query = e.Query[0:MAX_EXAMPLE_BYTES-3] + "..."
77+
} else {
78+
c.Example.Query = e.Query
79+
}
80+
c.Example.Ts = e.Ts
81+
}
82+
}
83+
}
84+
}
85+
86+
// Finalize calculates all metric statistics. Call this function when done
87+
// adding events to the class.
88+
func (c *Class) Finalize(rateLimit uint) {
89+
if rateLimit == 0 {
90+
rateLimit = 1
91+
}
92+
c.Metrics.Finalize(rateLimit)
93+
c.TotalQueries = (c.TotalQueries * rateLimit) + c.outliers
94+
if c.Example.QueryTime == 0 {
95+
c.Example = nil
96+
}
97+
}

0 commit comments

Comments
 (0)