Files
percona-toolkit/src/go/pt-mongodb-query-digest/main.go
2017-04-26 00:30:37 +02:00

538 lines
17 KiB
Go

package main
import (
"fmt"
"os"
"sort"
"strings"
"text/template"
"time"
"github.com/howeyc/gopass"
"github.com/pborman/getopt"
"github.com/percona/percona-toolkit/src/go/lib/config"
"github.com/percona/percona-toolkit/src/go/lib/versioncheck"
"github.com/percona/percona-toolkit/src/go/mongolib/fingerprinter"
"github.com/percona/percona-toolkit/src/go/mongolib/profiler"
"github.com/percona/percona-toolkit/src/go/mongolib/proto"
"github.com/percona/percona-toolkit/src/go/mongolib/stats"
"github.com/percona/percona-toolkit/src/go/mongolib/util"
"github.com/percona/percona-toolkit/src/go/pt-mongodb-query-digest/filter"
"github.com/percona/pmgo"
log "github.com/sirupsen/logrus"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
const (
TOOLNAME = "pt-mongodb-query-digest"
DEFAULT_AUTHDB = "admin"
DEFAULT_HOST = "localhost:27017"
DEFAULT_LOGLEVEL = "warn"
DEFAULT_ORDERBY = "-count" // comma separated list
DEFAULT_SKIPCOLLECTIONS = "system.profile" // comma separated list
)
var (
Build string = "01-01-1980"
GoVersion string = "1.8"
Version string = "3.0.1"
)
type options struct {
AuthDB string
Database string
Debug bool
Help bool
Host string
Limit int
LogLevel string
NoVersionCheck bool
OrderBy []string
Password string
SkipCollections []string
SSLCAFile string
SSLPEMKeyFile string
User string
Version bool
}
func main() {
opts, err := getOptions()
if err != nil {
log.Errorf("error processing commad line arguments: %s", err)
os.Exit(1)
}
if opts == nil && err == nil {
return
}
logLevel, err := log.ParseLevel(opts.LogLevel)
if err != nil {
fmt.Printf("Cannot set log level: %s", err.Error())
os.Exit(1)
}
log.SetLevel(logLevel)
if opts.Version {
fmt.Println(TOOLNAME)
fmt.Printf("Version %s\n", Version)
fmt.Printf("Build: %s using %s\n", Build, GoVersion)
return
}
conf := config.DefaultConfig(TOOLNAME)
if !conf.GetBool("no-version-check") && !opts.NoVersionCheck {
advice, err := versioncheck.CheckUpdates(TOOLNAME, Version)
if err != nil {
log.Infof("cannot check version updates: %s", err.Error())
} else {
if advice != "" {
log.Warn(advice)
}
}
}
di := getDialInfo(opts)
if di.Database == "" {
log.Errorln("must indicate a database as host:[port]/database")
getopt.PrintUsage(os.Stderr)
os.Exit(2)
}
dialer := pmgo.NewDialer()
session, err := dialer.DialWithInfo(di)
if err != nil {
log.Errorf("Error connecting to the db: %s while trying to connect to %s", err, di.Addrs[0])
os.Exit(3)
}
isProfilerEnabled, err := isProfilerEnabled(dialer, di)
if err != nil {
log.Errorf("Cannot get profiler status: %s", err.Error())
os.Exit(4)
}
if !isProfilerEnabled {
count, err := systemProfileDocsCount(session, di.Database)
if err != nil || count == 0 {
log.Error("Profiler is not enabled")
os.Exit(5)
}
fmt.Printf("Profiler is disabled for the %q database but there are %d documents in the system.profile collection.\n",
di.Database, count)
fmt.Println("Using those documents for the stats")
}
opts.SkipCollections = sanitizeSkipCollections(opts.SkipCollections)
filters := []filter.Filter{}
if len(opts.SkipCollections) > 0 {
filters = append(filters, filter.NewFilterByCollection(opts.SkipCollections))
}
query := bson.M{"op": bson.M{"$nin": []string{"getmore", "delete"}}}
i := session.DB(di.Database).C("system.profile").Find(query).Sort("-$natural").Iter()
fp := fingerprinter.NewFingerprinter(fingerprinter.DEFAULT_KEY_FILTERS)
s := stats.New(fp)
prof := profiler.NewProfiler(i, filters, nil, s)
prof.Start()
queries := <-prof.QueriesChan()
uptime := uptime(session)
queriesStats := queries.CalcQueriesStats(uptime)
sortedQueryStats := sortQueries(queriesStats, opts.OrderBy)
printHeader(opts)
queryTotals := queries.CalcTotalQueriesStats(uptime)
tt, _ := template.New("query").Funcs(template.FuncMap{
"Format": format,
}).Parse(getTotalsTemplate())
tt.Execute(os.Stdout, queryTotals)
t, _ := template.New("query").Funcs(template.FuncMap{
"Format": format,
}).Parse(getQueryTemplate())
if opts.Limit > 0 && len(sortedQueryStats) > opts.Limit {
sortedQueryStats = sortedQueryStats[:opts.Limit]
}
for _, qs := range sortedQueryStats {
t.Execute(os.Stdout, qs)
}
}
// format scales a number and returns a string made of the scaled value and unit (K=Kilo, M=Mega, T=Tera)
// using I.F where i is the number of digits for the integer part and F is the number of digits for the
// decimal part
// Examples:
// format(1000, 5.0) will return 1K
// format(1000, 5.2) will return 1.00k
func format(val float64, size float64) string {
units := []string{"K", "M", "T"}
unit := " "
intSize := int64(size)
decSize := int64((size - float64(intSize)) * 10)
for i := 0; i < 3; i++ {
if val > 1000 {
val /= 1000
unit = units[i]
}
}
pfmt := fmt.Sprintf("%% %d.%df", intSize, decSize)
fval := fmt.Sprintf(pfmt, val)
return fmt.Sprintf("%s%s", fval, unit)
}
func uptime(session pmgo.SessionManager) int64 {
ss := proto.ServerStatus{}
if err := session.Ping(); err != nil {
return 0
}
if err := session.DB("admin").Run(bson.D{{"serverStatus", 1}, {"recordStats", 1}}, &ss); err != nil {
return 0
}
return ss.Uptime
}
func getOptions() (*options, error) {
opts := &options{
Host: DEFAULT_HOST,
LogLevel: DEFAULT_LOGLEVEL,
OrderBy: strings.Split(DEFAULT_ORDERBY, ","),
SkipCollections: strings.Split(DEFAULT_SKIPCOLLECTIONS, ","),
AuthDB: DEFAULT_AUTHDB,
}
gop := getopt.New()
gop.BoolVarLong(&opts.Help, "help", '?', "Show help")
gop.BoolVarLong(&opts.Version, "version", 'v', "Show version & exit")
gop.BoolVarLong(&opts.NoVersionCheck, "no-version-check", 'c', "Default: Don't check for updates")
gop.IntVarLong(&opts.Limit, "limit", 'n', "Show the first n queries")
gop.ListVarLong(&opts.OrderBy, "order-by", 'o',
"Comma separated list of order by fields (max values): "+
"count,ratio,query-time,docs-scanned,docs-returned. "+
"- in front of the field name denotes reverse order. Default: "+DEFAULT_ORDERBY)
gop.ListVarLong(&opts.SkipCollections, "skip-collections", 's', "A comma separated list of collections (namespaces) to skip."+
" Default: "+DEFAULT_SKIPCOLLECTIONS)
gop.StringVarLong(&opts.AuthDB, "authenticationDatabase", 'a', "admin", "Database to use for optional MongoDB authentication. Default: admin")
gop.StringVarLong(&opts.Database, "database", 'd', "", "MongoDB database to profile")
gop.StringVarLong(&opts.LogLevel, "log-level", 'l', "Log level: error", "panic, fatal, error, warn, info, debug. Default: error")
gop.StringVarLong(&opts.Password, "password", 'p', "", "Password to use for optional MongoDB authentication").SetOptional()
gop.StringVarLong(&opts.User, "username", 'u', "Username to use for optional MongoDB authentication")
gop.StringVarLong(&opts.SSLCAFile, "sslCAFile", 0, "SSL CA cert file used for authentication")
gop.StringVarLong(&opts.SSLPEMKeyFile, "sslPEMKeyFile", 0, "SSL client PEM file used for authentication")
gop.SetParameters("host[:port]/database")
gop.Parse(os.Args)
if gop.NArgs() > 0 {
opts.Host = gop.Arg(0)
gop.Parse(gop.Args())
}
if opts.Help {
gop.PrintUsage(os.Stdout)
return nil, nil
}
if gop.IsSet("order-by") {
validFields := []string{"count", "ratio", "query-time", "docs-scanned", "docs-returned"}
for _, field := range opts.OrderBy {
valid := false
for _, vf := range validFields {
if field == vf || field == "-"+vf {
valid = true
}
}
if !valid {
return nil, fmt.Errorf("invalid sort field '%q'", field)
}
}
}
if gop.IsSet("password") && opts.Password == "" {
print("Password: ")
pass, err := gopass.GetPasswd()
if err != nil {
return nil, err
}
opts.Password = string(pass)
}
return opts, nil
}
func getDialInfo(opts *options) *pmgo.DialInfo {
di, _ := mgo.ParseURL(opts.Host)
di.FailFast = true
if di.Username != "" {
di.Username = opts.User
}
if di.Password != "" {
di.Password = opts.Password
}
if opts.AuthDB != "" {
di.Source = opts.AuthDB
}
if opts.Database != "" {
di.Database = opts.Database
}
pmgoDialInfo := pmgo.NewDialInfo(di)
if opts.SSLCAFile != "" {
pmgoDialInfo.SSLCAFile = opts.SSLCAFile
}
if opts.SSLPEMKeyFile != "" {
pmgoDialInfo.SSLPEMKeyFile = opts.SSLPEMKeyFile
}
return pmgoDialInfo
}
func printHeader(opts *options) {
fmt.Printf("%s - %s\n", TOOLNAME, time.Now().Format(time.RFC1123Z))
fmt.Printf("Host: %s\n", opts.Host)
fmt.Printf("Skipping profiled queries on these collections: %v\n", opts.SkipCollections)
fmt.Println("")
}
func getQueryTemplate() string {
t := `
# Query {{.Rank}}: {{printf "% 0.2f" .QPS}} QPS, ID {{.ID}}
# Ratio {{Format .Ratio 7.2}} (docs scanned/returned)
# Time range: {{.FirstSeen}} to {{.LastSeen}}
# Attribute pct total min max avg 95% stddev median
# ================== === ======== ======== ======== ======== ======== ======= ========
# Count (docs) {{printf "% 7d " .Count}}
# Exec Time ms {{printf "% 4.0f" .QueryTime.Pct}} {{printf "% 7.0f " .QueryTime.Total}} {{printf "% 7.0f " .QueryTime.Min}} {{printf "% 7.0f " .QueryTime.Max}} {{printf "% 7.0f " .QueryTime.Avg}} {{printf "% 7.0f " .QueryTime.Pct95}} {{printf "% 7.0f " .QueryTime.StdDev}} {{printf "% 7.0f " .QueryTime.Median}}
# Docs Scanned {{printf "% 4.0f" .Scanned.Pct}} {{Format .Scanned.Total 7.2}} {{Format .Scanned.Min 7.2}} {{Format .Scanned.Max 7.2}} {{Format .Scanned.Avg 7.2}} {{Format .Scanned.Pct95 7.2}} {{Format .Scanned.StdDev 7.2}} {{Format .Scanned.Median 7.2}}
# Docs Returned {{printf "% 4.0f" .Returned.Pct}} {{Format .Returned.Total 7.2}} {{Format .Returned.Min 7.2}} {{Format .Returned.Max 7.2}} {{Format .Returned.Avg 7.2}} {{Format .Returned.Pct95 7.2}} {{Format .Returned.StdDev 7.2}} {{Format .Returned.Median 7.2}}
# Bytes recv {{printf "% 4.0f" .ResponseLength.Pct}} {{Format .ResponseLength.Total 7.2}} {{Format .ResponseLength.Min 7.2}} {{Format .ResponseLength.Max 7.2}} {{Format .ResponseLength.Avg 7.2}} {{Format .ResponseLength.Pct95 7.2}} {{Format .ResponseLength.StdDev 7.2}} {{Format .ResponseLength.Median 7.2}}
# String:
# Namespaces {{.Namespace}}
# Operation {{.Operation}}
# Fingerprint {{.Fingerprint}}
# Query {{.Query}}
`
return t
}
func getTotalsTemplate() string {
t := `
# Totals
# Ratio {{Format .Ratio 7.2}} (docs scanned/returned)
# Attribute pct total min max avg 95% stddev median
# ================== === ======== ======== ======== ======== ======== ======= ========
# Count (docs) {{printf "% 7d " .Count}}
# Exec Time ms {{printf "% 4.0f" .QueryTime.Pct}} {{printf "% 7.0f " .QueryTime.Total}} {{printf "% 7.0f " .QueryTime.Min}} {{printf "% 7.0f " .QueryTime.Max}} {{printf "% 7.0f " .QueryTime.Avg}} {{printf "% 7.0f " .QueryTime.Pct95}} {{printf "% 7.0f " .QueryTime.StdDev}} {{printf "% 7.0f " .QueryTime.Median}}
# Docs Scanned {{printf "% 4.0f" .Scanned.Pct}} {{Format .Scanned.Total 7.2}} {{Format .Scanned.Min 7.2}} {{Format .Scanned.Max 7.2}} {{Format .Scanned.Avg 7.2}} {{Format .Scanned.Pct95 7.2}} {{Format .Scanned.StdDev 7.2}} {{Format .Scanned.Median 7.2}}
# Docs Returned {{printf "% 4.0f" .Returned.Pct}} {{Format .Returned.Total 7.2}} {{Format .Returned.Min 7.2}} {{Format .Returned.Max 7.2}} {{Format .Returned.Avg 7.2}} {{Format .Returned.Pct95 7.2}} {{Format .Returned.StdDev 7.2}} {{Format .Returned.Median 7.2}}
# Bytes recv {{printf "% 4.0f" .ResponseLength.Pct}} {{Format .ResponseLength.Total 7.2}} {{Format .ResponseLength.Min 7.2}} {{Format .ResponseLength.Max 7.2}} {{Format .ResponseLength.Avg 7.2}} {{Format .ResponseLength.Pct95 7.2}} {{Format .ResponseLength.StdDev 7.2}} {{Format .ResponseLength.Median 7.2}}
#
`
return t
}
type lessFunc func(p1, p2 *stats.QueryStats) bool
type multiSorter struct {
queries []stats.QueryStats
less []lessFunc
}
// Sort sorts the argument slice according to the less functions passed to OrderedBy.
func (ms *multiSorter) Sort(queries []stats.QueryStats) {
ms.queries = queries
sort.Sort(ms)
}
// OrderedBy returns a Sorter that sorts using the less functions, in order.
// Call its Sort method to sort the data.
func OrderedBy(less ...lessFunc) *multiSorter {
return &multiSorter{
less: less,
}
}
// Len is part of sort.Interface.
func (ms *multiSorter) Len() int {
return len(ms.queries)
}
// Swap is part of sort.Interface.
func (ms *multiSorter) Swap(i, j int) {
ms.queries[i], ms.queries[j] = ms.queries[j], ms.queries[i]
}
// Less is part of sort.Interface. It is implemented by looping along the
// less functions until it finds a comparison that is either Less or
// !Less. Note that it can call the less functions twice per call. We
// could change the functions to return -1, 0, 1 and reduce the
// number of calls for greater efficiency: an exercise for the reader.
func (ms *multiSorter) Less(i, j int) bool {
p, q := &ms.queries[i], &ms.queries[j]
// Try all but the last comparison.
var k int
for k = 0; k < len(ms.less)-1; k++ {
less := ms.less[k]
switch {
case less(p, q):
// p < q, so we have a decision.
return true
case less(q, p):
// p > q, so we have a decision.
return false
}
// p == q; try the next comparison.
}
// All comparisons to here said "equal", so just return whatever
// the final comparison reports.
return ms.less[k](p, q)
}
func sortQueries(queries []stats.QueryStats, orderby []string) []stats.QueryStats {
sortFuncs := []lessFunc{}
for _, field := range orderby {
var f lessFunc
switch field {
//
case "count":
f = func(c1, c2 *stats.QueryStats) bool {
return c1.Count < c2.Count
}
case "-count":
f = func(c1, c2 *stats.QueryStats) bool {
return c1.Count > c2.Count
}
case "ratio":
f = func(c1, c2 *stats.QueryStats) bool {
ratio1 := c1.Scanned.Max / c1.Returned.Max
ratio2 := c2.Scanned.Max / c2.Returned.Max
return ratio1 < ratio2
}
case "-ratio":
f = func(c1, c2 *stats.QueryStats) bool {
ratio1 := c1.Scanned.Max / c1.Returned.Max
ratio2 := c2.Scanned.Max / c2.Returned.Max
return ratio1 > ratio2
}
//
case "query-time":
f = func(c1, c2 *stats.QueryStats) bool {
return c1.QueryTime.Max < c2.QueryTime.Max
}
case "-query-time":
f = func(c1, c2 *stats.QueryStats) bool {
return c1.QueryTime.Max > c2.QueryTime.Max
}
//
case "docs-scanned":
f = func(c1, c2 *stats.QueryStats) bool {
return c1.Scanned.Max < c2.Scanned.Max
}
case "-docs-scanned":
f = func(c1, c2 *stats.QueryStats) bool {
return c1.Scanned.Max > c2.Scanned.Max
}
//
case "docs-returned":
f = func(c1, c2 *stats.QueryStats) bool {
return c1.Returned.Max < c2.Scanned.Max
}
case "-docs-returned":
f = func(c1, c2 *stats.QueryStats) bool {
return c1.Returned.Max > c2.Scanned.Max
}
}
// count,query-time,docs-scanned, docs-returned. - in front of the field name denotes reverse order.")
sortFuncs = append(sortFuncs, f)
}
OrderedBy(sortFuncs...).Sort(queries)
return queries
}
func isProfilerEnabled(dialer pmgo.Dialer, di *pmgo.DialInfo) (bool, error) {
var ps proto.ProfilerStatus
replicaMembers, err := util.GetReplicasetMembers(dialer, di)
if err != nil {
return false, err
}
for _, member := range replicaMembers {
// Stand alone instances return state = REPLICA_SET_MEMBER_STARTUP
di.Addrs = []string{member.Name}
session, err := dialer.DialWithInfo(di)
if err != nil {
continue
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
isReplicaEnabled := isReplicasetEnabled(session)
if strings.ToLower(member.StateStr) == "configsvr" {
continue
}
if isReplicaEnabled && member.State != proto.REPLICA_SET_MEMBER_PRIMARY {
continue
}
if err := session.DB(di.Database).Run(bson.M{"profile": -1}, &ps); err != nil {
continue
}
if ps.Was == 0 {
return false, nil
}
}
return true, nil
}
func systemProfileDocsCount(session pmgo.SessionManager, dbname string) (int, error) {
return session.DB(dbname).C("system.profile").Count()
}
func isReplicasetEnabled(session pmgo.SessionManager) bool {
rss := proto.ReplicaSetStatus{}
if err := session.Run(bson.M{"replSetGetStatus": 1}, &rss); err != nil {
return false
}
return true
}
// Sanitize the param. using --skip-collections="" will produce an 1 element array but
// that element will be empty. The same would be using --skip-collections=a,,d
func sanitizeSkipCollections(skipCollections []string) []string {
cols := []string{}
if len(skipCollections) > 0 {
for _, c := range skipCollections {
if strings.TrimSpace(c) != "" {
cols = append(cols, c)
}
}
}
return cols
}