mirror of
https://github.com/percona/percona-toolkit.git
synced 2025-09-12 22:19:44 +00:00
WIP
This commit is contained in:
115
src/go/mongolib/fingerprinter/figerprinter.go
Normal file
115
src/go/mongolib/fingerprinter/figerprinter.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package fingerprinter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/percona/percona-toolkit/src/go/mongolib/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
MAX_DEPTH_LEVEL = 10
|
||||||
|
DEFAULT_KEY_FILTERS = []string{"^shardVersion$", "^\\$"}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Fingerprinter interface {
|
||||||
|
Fingerprint(query map[string]interface{}) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Fingerprint struct {
|
||||||
|
keyFilters []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFingerprinter(keyFilters []string) *Fingerprint {
|
||||||
|
return &Fingerprint{
|
||||||
|
keyFilters: keyFilters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query is the top level map query element
|
||||||
|
// Example for MongoDB 3.2+
|
||||||
|
// "query" : {
|
||||||
|
// "find" : "col1",
|
||||||
|
// "filter" : {
|
||||||
|
// "s2" : {
|
||||||
|
// "$lt" : "54701",
|
||||||
|
// "$gte" : "73754"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "sort" : {
|
||||||
|
// "user_id" : 1
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
func (f *Fingerprint) Fingerprint(query map[string]interface{}) (string, error) {
|
||||||
|
|
||||||
|
realQuery, err := util.GetQueryField(query)
|
||||||
|
if err != nil {
|
||||||
|
// Try to encode doc.Query as json for prettiness
|
||||||
|
if buf, err := json.Marshal(realQuery); err == nil {
|
||||||
|
return "", fmt.Errorf("%v for query %s", err, string(buf))
|
||||||
|
}
|
||||||
|
// If we cannot encode as json, return just the error message without the query
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
retKeys := keys(realQuery, f.keyFilters)
|
||||||
|
|
||||||
|
sort.Strings(retKeys)
|
||||||
|
|
||||||
|
// if there is a sort clause in the query, we have to add all fields in the sort
|
||||||
|
// fields list that are not in the query keys list (retKeys)
|
||||||
|
if sortKeys, ok := query["sort"]; ok {
|
||||||
|
if sortKeysMap, ok := sortKeys.(map[string]interface{}); ok {
|
||||||
|
sortKeys := keys(sortKeysMap, f.keyFilters)
|
||||||
|
for _, sortKey := range sortKeys {
|
||||||
|
if !inSlice(sortKey, retKeys) {
|
||||||
|
retKeys = append(retKeys, sortKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(retKeys, ","), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func inSlice(str string, list []string) bool {
|
||||||
|
for _, v := range list {
|
||||||
|
if v == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func keys(query map[string]interface{}, keyFilters []string) []string {
|
||||||
|
return getKeys(query, keyFilters, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeys(query map[string]interface{}, keyFilters []string, level int) []string {
|
||||||
|
ks := []string{}
|
||||||
|
for key, value := range query {
|
||||||
|
if shouldSkipKey(key, keyFilters) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ks = append(ks, key)
|
||||||
|
if m, ok := value.(map[string]interface{}); ok {
|
||||||
|
level++
|
||||||
|
if level <= MAX_DEPTH_LEVEL {
|
||||||
|
ks = append(ks, getKeys(m, keyFilters, level)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(ks)
|
||||||
|
return ks
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldSkipKey(key string, keyFilters []string) bool {
|
||||||
|
for _, filter := range keyFilters {
|
||||||
|
if matched, _ := regexp.MatchString(filter, key); matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
176
src/go/mongolib/profiler/profiler.go
Normal file
176
src/go/mongolib/profiler/profiler.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package profiler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/percona/percona-toolkit/src/go/mongolib/fingerprinter"
|
||||||
|
"github.com/percona/percona-toolkit/src/go/mongolib/proto"
|
||||||
|
"github.com/percona/percona-toolkit/src/go/mongolib/util"
|
||||||
|
"github.com/percona/percona-toolkit/src/go/pt-mongodb-query-digest/filter"
|
||||||
|
"github.com/prometheus/common/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
MAX_DEPTH_LEVEL = 10
|
||||||
|
CANNOT_GET_QUERY_ERROR = errors.New("cannot get query field from the profile document (it is not a map)")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Times []time.Time
|
||||||
|
|
||||||
|
func (a Times) Len() int { return len(a) }
|
||||||
|
func (a Times) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a Times) Less(i, j int) bool { return a[i].Before(a[j]) }
|
||||||
|
|
||||||
|
type StatsGroupKey struct {
|
||||||
|
Operation string
|
||||||
|
Fingerprint string
|
||||||
|
Namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stat struct {
|
||||||
|
BlockedTime Times
|
||||||
|
Count int
|
||||||
|
Fingerprint string
|
||||||
|
FirstSeen time.Time
|
||||||
|
ID string
|
||||||
|
LastSeen time.Time
|
||||||
|
LockTime Times
|
||||||
|
NReturned []float64
|
||||||
|
NScanned []float64
|
||||||
|
Namespace string
|
||||||
|
Operation string
|
||||||
|
Query map[string]interface{}
|
||||||
|
QueryTime []float64 // in milliseconds
|
||||||
|
ResponseLength []float64
|
||||||
|
TableScan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Iter interface {
|
||||||
|
All(result interface{}) error
|
||||||
|
Close() error
|
||||||
|
Err() error
|
||||||
|
For(result interface{}, f func() error) (err error)
|
||||||
|
Next(result interface{}) bool
|
||||||
|
Timeout() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Profiler interface {
|
||||||
|
StatsChan() chan []Stat
|
||||||
|
Start()
|
||||||
|
Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
filters []filter.Filter
|
||||||
|
iterator Iter
|
||||||
|
ticker chan time.Time
|
||||||
|
statsChan chan []Stat
|
||||||
|
stopChan chan bool
|
||||||
|
stats []Stat
|
||||||
|
keyFilters []string
|
||||||
|
fingerprinter fingerprinter.Fingerprinter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProfiler(iterator Iter, filters []filter.Filter, ticker chan time.Time, fp fingerprinter.Fingerprinter) Profiler {
|
||||||
|
return &Profile{
|
||||||
|
filters: filters,
|
||||||
|
fingerprinter: fp,
|
||||||
|
iterator: iterator,
|
||||||
|
ticker: ticker,
|
||||||
|
statsChan: make(chan []Stat),
|
||||||
|
stats: make([]Stat, 100),
|
||||||
|
keyFilters: []string{"^shardVersion$", "^\\$"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) StatsChan() chan []Stat {
|
||||||
|
return p.statsChan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Start() {
|
||||||
|
go p.getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) Stop() {
|
||||||
|
p.stopChan <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profile) getData() {
|
||||||
|
var doc proto.SystemProfile
|
||||||
|
stop := false
|
||||||
|
stats := make(map[StatsGroupKey]*Stat)
|
||||||
|
|
||||||
|
for !stop && p.iterator.Next(&doc) && p.iterator.Err() == nil {
|
||||||
|
valid := true
|
||||||
|
for _, filter := range p.filters {
|
||||||
|
if filter(doc) == false {
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-p.ticker:
|
||||||
|
p.statsChan <- statsToArray(stats)
|
||||||
|
case <-p.stopChan:
|
||||||
|
stop = true
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
if len(doc.Query) > 0 {
|
||||||
|
|
||||||
|
fp, err := p.fingerprinter.Fingerprint(doc.Query)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("cannot get fingerprint: %s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var s *Stat
|
||||||
|
var ok bool
|
||||||
|
key := StatsGroupKey{
|
||||||
|
Operation: doc.Op,
|
||||||
|
Fingerprint: fp,
|
||||||
|
Namespace: doc.Ns,
|
||||||
|
}
|
||||||
|
if s, ok = stats[key]; !ok {
|
||||||
|
realQuery, _ := util.GetQueryField(doc.Query)
|
||||||
|
s = &Stat{
|
||||||
|
ID: fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s", key)))),
|
||||||
|
Operation: doc.Op,
|
||||||
|
Fingerprint: fp,
|
||||||
|
Namespace: doc.Ns,
|
||||||
|
TableScan: false,
|
||||||
|
Query: realQuery,
|
||||||
|
}
|
||||||
|
stats[key] = s
|
||||||
|
}
|
||||||
|
s.Count++
|
||||||
|
s.NScanned = append(s.NScanned, float64(doc.DocsExamined))
|
||||||
|
s.NReturned = append(s.NReturned, float64(doc.Nreturned))
|
||||||
|
s.QueryTime = append(s.QueryTime, float64(doc.Millis))
|
||||||
|
s.ResponseLength = append(s.ResponseLength, float64(doc.ResponseLength))
|
||||||
|
var zeroTime time.Time
|
||||||
|
if s.FirstSeen == zeroTime || s.FirstSeen.After(doc.Ts) {
|
||||||
|
s.FirstSeen = doc.Ts
|
||||||
|
}
|
||||||
|
if s.LastSeen == zeroTime || s.LastSeen.Before(doc.Ts) {
|
||||||
|
s.LastSeen = doc.Ts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.statsChan <- statsToArray(stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statsToArray(stats map[StatsGroupKey]*Stat) []Stat {
|
||||||
|
sa := make([]Stat, len(stats))
|
||||||
|
for _, s := range stats {
|
||||||
|
sa = append(sa, *s)
|
||||||
|
}
|
||||||
|
return sa
|
||||||
|
}
|
@@ -12,6 +12,10 @@ import (
|
|||||||
"gopkg.in/mgo.v2/bson"
|
"gopkg.in/mgo.v2/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
CANNOT_GET_QUERY_ERROR = errors.New("cannot get query field from the profile document (it is not a map)")
|
||||||
|
)
|
||||||
|
|
||||||
func GetReplicasetMembers(dialer pmgo.Dialer, di *pmgo.DialInfo) ([]proto.Members, error) {
|
func GetReplicasetMembers(dialer pmgo.Dialer, di *pmgo.DialInfo) ([]proto.Members, error) {
|
||||||
hostnames, err := GetHostnames(dialer, di)
|
hostnames, err := GetHostnames(dialer, di)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -232,3 +236,22 @@ func GetServerStatus(dialer pmgo.Dialer, di *pmgo.DialInfo, hostname string) (pr
|
|||||||
|
|
||||||
return ss, nil
|
return ss, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetQueryField(query map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
// MongoDB 3.0
|
||||||
|
if squery, ok := query["$query"]; ok {
|
||||||
|
// just an extra check to ensure this type assertion won't fail
|
||||||
|
if ssquery, ok := squery.(map[string]interface{}); ok {
|
||||||
|
return ssquery, nil
|
||||||
|
}
|
||||||
|
return nil, CANNOT_GET_QUERY_ERROR
|
||||||
|
}
|
||||||
|
// MongoDB 3.2+
|
||||||
|
if squery, ok := query["filter"]; ok {
|
||||||
|
if ssquery, ok := squery.(map[string]interface{}); ok {
|
||||||
|
return ssquery, nil
|
||||||
|
}
|
||||||
|
return nil, CANNOT_GET_QUERY_ERROR
|
||||||
|
}
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
23
src/go/pt-mongodb-query-digest/filter/filters.go
Normal file
23
src/go/pt-mongodb-query-digest/filter/filters.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/percona/percona-toolkit/src/go/mongolib/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Filter func(proto.SystemProfile) bool
|
||||||
|
|
||||||
|
// This func receives a doc from the profiler and returns:
|
||||||
|
// true : the document must be considered
|
||||||
|
// false: the document must be skipped
|
||||||
|
func NewFilterByCollection(collectionsToSkip []string) func(proto.SystemProfile) bool {
|
||||||
|
return func(doc proto.SystemProfile) bool {
|
||||||
|
for _, collection := range collectionsToSkip {
|
||||||
|
if strings.HasSuffix(doc.Ns, collection) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@@ -1,25 +1,24 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/howeyc/gopass"
|
"github.com/howeyc/gopass"
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/montanaflynn/stats"
|
"github.com/montanaflynn/stats"
|
||||||
"github.com/pborman/getopt"
|
"github.com/pborman/getopt"
|
||||||
"github.com/percona/percona-toolkit/src/go/lib/config"
|
"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/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/proto"
|
||||||
"github.com/percona/percona-toolkit/src/go/mongolib/util"
|
"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"
|
"github.com/percona/pmgo"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/mgo.v2"
|
"gopkg.in/mgo.v2"
|
||||||
@@ -27,8 +26,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TOOLNAME = "pt-mongodb-query-digest"
|
TOOLNAME = "pt-mongodb-query-digest"
|
||||||
MAX_DEPTH_LEVEL = 10
|
|
||||||
|
|
||||||
DEFAULT_AUTHDB = "admin"
|
DEFAULT_AUTHDB = "admin"
|
||||||
DEFAULT_HOST = "localhost:27017"
|
DEFAULT_HOST = "localhost:27017"
|
||||||
@@ -41,24 +39,8 @@ var (
|
|||||||
Build string = "01-01-1980"
|
Build string = "01-01-1980"
|
||||||
GoVersion string = "1.8"
|
GoVersion string = "1.8"
|
||||||
Version string = "3.0.1"
|
Version string = "3.0.1"
|
||||||
|
|
||||||
CANNOT_GET_QUERY_ERROR = errors.New("cannot get query field from the profile document (it is not a map)")
|
|
||||||
|
|
||||||
// This is a regexp array to filter out the keys we don't want in the fingerprint
|
|
||||||
keyFilters = func() []string {
|
|
||||||
return []string{"^shardVersion$", "^\\$"}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type iter interface {
|
|
||||||
All(result interface{}) error
|
|
||||||
Close() error
|
|
||||||
Err() error
|
|
||||||
For(result interface{}, f func() error) (err error)
|
|
||||||
Next(result interface{}) bool
|
|
||||||
Timeout() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type options struct {
|
type options struct {
|
||||||
AuthDB string
|
AuthDB string
|
||||||
Database string
|
Database string
|
||||||
@@ -82,42 +64,6 @@ type options struct {
|
|||||||
// false: the document must be skipped
|
// false: the document must be skipped
|
||||||
type docsFilter func(proto.SystemProfile) bool
|
type docsFilter func(proto.SystemProfile) bool
|
||||||
|
|
||||||
type statsArray []stat
|
|
||||||
|
|
||||||
func (a statsArray) Len() int { return len(a) }
|
|
||||||
func (a statsArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
||||||
func (a statsArray) Less(i, j int) bool { return a[i].Count < a[j].Count }
|
|
||||||
|
|
||||||
type times []time.Time
|
|
||||||
|
|
||||||
func (a times) Len() int { return len(a) }
|
|
||||||
func (a times) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
||||||
func (a times) Less(i, j int) bool { return a[i].Before(a[j]) }
|
|
||||||
|
|
||||||
type stat struct {
|
|
||||||
ID string
|
|
||||||
Operation string
|
|
||||||
Fingerprint string
|
|
||||||
Namespace string
|
|
||||||
Query map[string]interface{}
|
|
||||||
Count int
|
|
||||||
TableScan bool
|
|
||||||
NScanned []float64
|
|
||||||
NReturned []float64
|
|
||||||
QueryTime []float64 // in milliseconds
|
|
||||||
ResponseLength []float64
|
|
||||||
LockTime times
|
|
||||||
BlockedTime times
|
|
||||||
FirstSeen time.Time
|
|
||||||
LastSeen time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type groupKey struct {
|
|
||||||
Operation string
|
|
||||||
Fingerprint string
|
|
||||||
Namespace string
|
|
||||||
}
|
|
||||||
|
|
||||||
type statistics struct {
|
type statistics struct {
|
||||||
Pct float64
|
Pct float64
|
||||||
Total float64
|
Total float64
|
||||||
@@ -215,36 +161,22 @@ func main() {
|
|||||||
fmt.Println("Using those documents for the stats")
|
fmt.Println("Using those documents for the stats")
|
||||||
}
|
}
|
||||||
|
|
||||||
filters := []docsFilter{}
|
opts.SkipCollections = sanitizeSkipCollections(opts.SkipCollections)
|
||||||
|
filters := []filter.Filter{}
|
||||||
|
|
||||||
if len(opts.SkipCollections) > 0 {
|
if len(opts.SkipCollections) > 0 {
|
||||||
// Sanitize the param. using --skip-collections="" will produce an 1 element array but
|
filters = append(filters, filter.NewFilterByCollection(opts.SkipCollections))
|
||||||
// that element will be empty. The same would be using --skip-collections=a,,d
|
|
||||||
cols := []string{}
|
|
||||||
for _, c := range opts.SkipCollections {
|
|
||||||
if strings.TrimSpace(c) != "" {
|
|
||||||
cols = append(cols, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(cols) > 0 {
|
|
||||||
// This func receives a doc from the profiler and returns:
|
|
||||||
// true : the document must be considered
|
|
||||||
// false: the document must be skipped
|
|
||||||
filterSystemProfile := func(doc proto.SystemProfile) bool {
|
|
||||||
for _, collection := range cols {
|
|
||||||
if strings.HasSuffix(doc.Ns, collection) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
filters = append(filters, filterSystemProfile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query := bson.M{"op": bson.M{"$nin": []string{"getmore", "delete"}}}
|
query := bson.M{"op": bson.M{"$nin": []string{"getmore", "delete"}}}
|
||||||
i := session.DB(di.Database).C("system.profile").Find(query).Sort("-$natural").Iter()
|
i := session.DB(di.Database).C("system.profile").Find(query).Sort("-$natural").Iter()
|
||||||
queries := sortQueries(getData(i, filters), opts.OrderBy)
|
|
||||||
|
fp := fingerprinter.NewFingerprinter(fingerprinter.DEFAULT_KEY_FILTERS)
|
||||||
|
prof := profiler.NewProfiler(i, filters, nil, fp)
|
||||||
|
prof.Start()
|
||||||
|
queries := <-prof.StatsChan()
|
||||||
|
|
||||||
|
queries = sortQueries(queries, opts.OrderBy)
|
||||||
|
|
||||||
uptime := uptime(session)
|
uptime := uptime(session)
|
||||||
|
|
||||||
@@ -306,9 +238,9 @@ func uptime(session pmgo.SessionManager) int64 {
|
|||||||
return ss.Uptime
|
return ss.Uptime
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcTotalQueryStats(queries []stat, uptime int64) queryInfo {
|
func calcTotalQueryStats(queries []profiler.Stat, uptime int64) queryInfo {
|
||||||
qi := queryInfo{}
|
qi := queryInfo{}
|
||||||
qs := stat{}
|
qs := profiler.Stat{}
|
||||||
_, totalScanned, totalReturned, totalQueryTime, totalBytes := calcTotals(queries)
|
_, totalScanned, totalReturned, totalQueryTime, totalBytes := calcTotals(queries)
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
qs.NScanned = append(qs.NScanned, query.NScanned...)
|
qs.NScanned = append(qs.NScanned, query.NScanned...)
|
||||||
@@ -342,7 +274,7 @@ func calcTotalQueryStats(queries []stat, uptime int64) queryInfo {
|
|||||||
return qi
|
return qi
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcQueryStats(queries []stat, uptime int64) []queryInfo {
|
func calcQueryStats(queries []profiler.Stat, uptime int64) []queryInfo {
|
||||||
queryStats := []queryInfo{}
|
queryStats := []queryInfo{}
|
||||||
_, totalScanned, totalReturned, totalQueryTime, totalBytes := calcTotals(queries)
|
_, totalScanned, totalReturned, totalQueryTime, totalBytes := calcTotals(queries)
|
||||||
for rank, query := range queries {
|
for rank, query := range queries {
|
||||||
@@ -383,9 +315,9 @@ func calcQueryStats(queries []stat, uptime int64) []queryInfo {
|
|||||||
return queryStats
|
return queryStats
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTotals(queries []stat) stat {
|
func getTotals(queries []profiler.Stat) profiler.Stat {
|
||||||
|
|
||||||
qt := stat{}
|
qt := profiler.Stat{}
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
qt.NScanned = append(qt.NScanned, query.NScanned...)
|
qt.NScanned = append(qt.NScanned, query.NScanned...)
|
||||||
qt.NReturned = append(qt.NReturned, query.NReturned...)
|
qt.NReturned = append(qt.NReturned, query.NReturned...)
|
||||||
@@ -396,7 +328,7 @@ func getTotals(queries []stat) stat {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcTotals(queries []stat) (totalCount int, totalScanned, totalReturned, totalQueryTime, totalBytes float64) {
|
func calcTotals(queries []profiler.Stat) (totalCount int, totalScanned, totalReturned, totalQueryTime, totalBytes float64) {
|
||||||
|
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
totalCount += query.Count
|
totalCount += query.Count
|
||||||
@@ -428,78 +360,6 @@ func calcStats(samples []float64) statistics {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func getData(i iter, filters []docsFilter) []stat {
|
|
||||||
var doc proto.SystemProfile
|
|
||||||
stats := make(map[groupKey]*stat)
|
|
||||||
|
|
||||||
log.Debug(`Documents returned by db.getSiblinfDB("<dbnamehere>").system.profile.Find({"op": {"$nin": []string{"getmore", "delete"}}).Sort("-$natural")`)
|
|
||||||
|
|
||||||
for i.Next(&doc) && i.Err() == nil {
|
|
||||||
valid := true
|
|
||||||
for _, filter := range filters {
|
|
||||||
if filter(doc) == false {
|
|
||||||
valid = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !valid {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugln("====================================================================================================")
|
|
||||||
log.Debug(pretty.Sprint(doc))
|
|
||||||
if len(doc.Query) > 0 {
|
|
||||||
|
|
||||||
fp, err := fingerprint(doc.Query)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("cannot get fingerprint: %s", err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var s *stat
|
|
||||||
var ok bool
|
|
||||||
key := groupKey{
|
|
||||||
Operation: doc.Op,
|
|
||||||
Fingerprint: fp,
|
|
||||||
Namespace: doc.Ns,
|
|
||||||
}
|
|
||||||
if s, ok = stats[key]; !ok {
|
|
||||||
realQuery, _ := getQueryField(doc.Query)
|
|
||||||
s = &stat{
|
|
||||||
ID: fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s", key)))),
|
|
||||||
Operation: doc.Op,
|
|
||||||
Fingerprint: fp,
|
|
||||||
Namespace: doc.Ns,
|
|
||||||
TableScan: false,
|
|
||||||
Query: realQuery,
|
|
||||||
}
|
|
||||||
stats[key] = s
|
|
||||||
}
|
|
||||||
s.Count++
|
|
||||||
s.NScanned = append(s.NScanned, float64(doc.DocsExamined))
|
|
||||||
s.NReturned = append(s.NReturned, float64(doc.Nreturned))
|
|
||||||
s.QueryTime = append(s.QueryTime, float64(doc.Millis))
|
|
||||||
s.ResponseLength = append(s.ResponseLength, float64(doc.ResponseLength))
|
|
||||||
var zeroTime time.Time
|
|
||||||
if s.FirstSeen == zeroTime || s.FirstSeen.After(doc.Ts) {
|
|
||||||
s.FirstSeen = doc.Ts
|
|
||||||
}
|
|
||||||
if s.LastSeen == zeroTime || s.LastSeen.Before(doc.Ts) {
|
|
||||||
s.LastSeen = doc.Ts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to sort the data but a hash cannot be sorted so, convert the hash having
|
|
||||||
// the results to a slice
|
|
||||||
sa := statsArray{}
|
|
||||||
for _, s := range stats {
|
|
||||||
sa = append(sa, *s)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(sa)
|
|
||||||
return sa
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOptions() (*options, error) {
|
func getOptions() (*options, error) {
|
||||||
opts := &options{
|
opts := &options{
|
||||||
Host: DEFAULT_HOST,
|
Host: DEFAULT_HOST,
|
||||||
@@ -600,121 +460,6 @@ func getDialInfo(opts *options) *pmgo.DialInfo {
|
|||||||
return pmgoDialInfo
|
return pmgoDialInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
func getQueryField(query map[string]interface{}) (map[string]interface{}, error) {
|
|
||||||
// MongoDB 3.0
|
|
||||||
if squery, ok := query["$query"]; ok {
|
|
||||||
// just an extra check to ensure this type assertion won't fail
|
|
||||||
if ssquery, ok := squery.(map[string]interface{}); ok {
|
|
||||||
return ssquery, nil
|
|
||||||
}
|
|
||||||
return nil, CANNOT_GET_QUERY_ERROR
|
|
||||||
}
|
|
||||||
// MongoDB 3.2+
|
|
||||||
if squery, ok := query["filter"]; ok {
|
|
||||||
if ssquery, ok := squery.(map[string]interface{}); ok {
|
|
||||||
return ssquery, nil
|
|
||||||
}
|
|
||||||
return nil, CANNOT_GET_QUERY_ERROR
|
|
||||||
}
|
|
||||||
return query, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query is the top level map query element
|
|
||||||
// Example for MongoDB 3.2+
|
|
||||||
// "query" : {
|
|
||||||
// "find" : "col1",
|
|
||||||
// "filter" : {
|
|
||||||
// "s2" : {
|
|
||||||
// "$lt" : "54701",
|
|
||||||
// "$gte" : "73754"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "sort" : {
|
|
||||||
// "user_id" : 1
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
func fingerprint(query map[string]interface{}) (string, error) {
|
|
||||||
|
|
||||||
realQuery, err := getQueryField(query)
|
|
||||||
if err != nil {
|
|
||||||
// Try to encode doc.Query as json for prettiness
|
|
||||||
if buf, err := json.Marshal(realQuery); err == nil {
|
|
||||||
return "", fmt.Errorf("%v for query %s", err, string(buf))
|
|
||||||
}
|
|
||||||
// If we cannot encode as json, return just the error message without the query
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
retKeys := keys(realQuery, 0)
|
|
||||||
|
|
||||||
sort.Strings(retKeys)
|
|
||||||
|
|
||||||
// if there is a sort clause in the query, we have to add all fields in the sort
|
|
||||||
// fields list that are not in the query keys list (retKeys)
|
|
||||||
if sortKeys, ok := query["sort"]; ok {
|
|
||||||
if sortKeysMap, ok := sortKeys.(map[string]interface{}); ok {
|
|
||||||
sortKeys := mapKeys(sortKeysMap, 0)
|
|
||||||
for _, sortKey := range sortKeys {
|
|
||||||
if !inSlice(sortKey, retKeys) {
|
|
||||||
retKeys = append(retKeys, sortKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(retKeys, ","), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func inSlice(str string, list []string) bool {
|
|
||||||
for _, v := range list {
|
|
||||||
if v == str {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func keys(query map[string]interface{}, level int) []string {
|
|
||||||
ks := []string{}
|
|
||||||
for key, value := range query {
|
|
||||||
if shouldSkipKey(key) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ks = append(ks, key)
|
|
||||||
if m, ok := value.(map[string]interface{}); ok {
|
|
||||||
level++
|
|
||||||
if level <= MAX_DEPTH_LEVEL {
|
|
||||||
ks = append(ks, keys(m, level)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(ks)
|
|
||||||
return ks
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapKeys(query map[string]interface{}, level int) []string {
|
|
||||||
ks := []string{}
|
|
||||||
for key, value := range query {
|
|
||||||
ks = append(ks, key)
|
|
||||||
if m, ok := value.(map[string]interface{}); ok {
|
|
||||||
level++
|
|
||||||
if level <= MAX_DEPTH_LEVEL {
|
|
||||||
ks = append(ks, keys(m, level)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(ks)
|
|
||||||
return ks
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldSkipKey(key string) bool {
|
|
||||||
for _, filter := range keyFilters() {
|
|
||||||
if matched, _ := regexp.MatchString(filter, key); matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func printHeader(opts *options) {
|
func printHeader(opts *options) {
|
||||||
fmt.Printf("%s - %s\n", TOOLNAME, time.Now().Format(time.RFC1123Z))
|
fmt.Printf("%s - %s\n", TOOLNAME, time.Now().Format(time.RFC1123Z))
|
||||||
fmt.Printf("Host: %s\n", opts.Host)
|
fmt.Printf("Host: %s\n", opts.Host)
|
||||||
@@ -760,15 +505,15 @@ func getTotalsTemplate() string {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
type lessFunc func(p1, p2 *stat) bool
|
type lessFunc func(p1, p2 *profiler.Stat) bool
|
||||||
|
|
||||||
type multiSorter struct {
|
type multiSorter struct {
|
||||||
queries []stat
|
queries []profiler.Stat
|
||||||
less []lessFunc
|
less []lessFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort sorts the argument slice according to the less functions passed to OrderedBy.
|
// Sort sorts the argument slice according to the less functions passed to OrderedBy.
|
||||||
func (ms *multiSorter) Sort(queries []stat) {
|
func (ms *multiSorter) Sort(queries []profiler.Stat) {
|
||||||
ms.queries = queries
|
ms.queries = queries
|
||||||
sort.Sort(ms)
|
sort.Sort(ms)
|
||||||
}
|
}
|
||||||
@@ -817,23 +562,23 @@ func (ms *multiSorter) Less(i, j int) bool {
|
|||||||
return ms.less[k](p, q)
|
return ms.less[k](p, q)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortQueries(queries []stat, orderby []string) []stat {
|
func sortQueries(queries []profiler.Stat, orderby []string) []profiler.Stat {
|
||||||
sortFuncs := []lessFunc{}
|
sortFuncs := []lessFunc{}
|
||||||
for _, field := range orderby {
|
for _, field := range orderby {
|
||||||
var f lessFunc
|
var f lessFunc
|
||||||
switch field {
|
switch field {
|
||||||
//
|
//
|
||||||
case "count":
|
case "count":
|
||||||
f = func(c1, c2 *stat) bool {
|
f = func(c1, c2 *profiler.Stat) bool {
|
||||||
return c1.Count < c2.Count
|
return c1.Count < c2.Count
|
||||||
}
|
}
|
||||||
case "-count":
|
case "-count":
|
||||||
f = func(c1, c2 *stat) bool {
|
f = func(c1, c2 *profiler.Stat) bool {
|
||||||
return c1.Count > c2.Count
|
return c1.Count > c2.Count
|
||||||
}
|
}
|
||||||
|
|
||||||
case "ratio":
|
case "ratio":
|
||||||
f = func(c1, c2 *stat) bool {
|
f = func(c1, c2 *profiler.Stat) bool {
|
||||||
ns1, _ := stats.Max(c1.NScanned)
|
ns1, _ := stats.Max(c1.NScanned)
|
||||||
ns2, _ := stats.Max(c2.NScanned)
|
ns2, _ := stats.Max(c2.NScanned)
|
||||||
nr1, _ := stats.Max(c1.NReturned)
|
nr1, _ := stats.Max(c1.NReturned)
|
||||||
@@ -843,7 +588,7 @@ func sortQueries(queries []stat, orderby []string) []stat {
|
|||||||
return ratio1 < ratio2
|
return ratio1 < ratio2
|
||||||
}
|
}
|
||||||
case "-ratio":
|
case "-ratio":
|
||||||
f = func(c1, c2 *stat) bool {
|
f = func(c1, c2 *profiler.Stat) bool {
|
||||||
ns1, _ := stats.Max(c1.NScanned)
|
ns1, _ := stats.Max(c1.NScanned)
|
||||||
ns2, _ := stats.Max(c2.NScanned)
|
ns2, _ := stats.Max(c2.NScanned)
|
||||||
nr1, _ := stats.Max(c1.NReturned)
|
nr1, _ := stats.Max(c1.NReturned)
|
||||||
@@ -855,13 +600,13 @@ func sortQueries(queries []stat, orderby []string) []stat {
|
|||||||
|
|
||||||
//
|
//
|
||||||
case "query-time":
|
case "query-time":
|
||||||
f = func(c1, c2 *stat) bool {
|
f = func(c1, c2 *profiler.Stat) bool {
|
||||||
qt1, _ := stats.Max(c1.QueryTime)
|
qt1, _ := stats.Max(c1.QueryTime)
|
||||||
qt2, _ := stats.Max(c2.QueryTime)
|
qt2, _ := stats.Max(c2.QueryTime)
|
||||||
return qt1 < qt2
|
return qt1 < qt2
|
||||||
}
|
}
|
||||||
case "-query-time":
|
case "-query-time":
|
||||||
f = func(c1, c2 *stat) bool {
|
f = func(c1, c2 *profiler.Stat) bool {
|
||||||
qt1, _ := stats.Max(c1.QueryTime)
|
qt1, _ := stats.Max(c1.QueryTime)
|
||||||
qt2, _ := stats.Max(c2.QueryTime)
|
qt2, _ := stats.Max(c2.QueryTime)
|
||||||
return qt1 > qt2
|
return qt1 > qt2
|
||||||
@@ -869,13 +614,13 @@ func sortQueries(queries []stat, orderby []string) []stat {
|
|||||||
|
|
||||||
//
|
//
|
||||||
case "docs-scanned":
|
case "docs-scanned":
|
||||||
f = func(c1, c2 *stat) bool {
|
f = func(c1, c2 *profiler.Stat) bool {
|
||||||
ns1, _ := stats.Max(c1.NScanned)
|
ns1, _ := stats.Max(c1.NScanned)
|
||||||
ns2, _ := stats.Max(c2.NScanned)
|
ns2, _ := stats.Max(c2.NScanned)
|
||||||
return ns1 < ns2
|
return ns1 < ns2
|
||||||
}
|
}
|
||||||
case "-docs-scanned":
|
case "-docs-scanned":
|
||||||
f = func(c1, c2 *stat) bool {
|
f = func(c1, c2 *profiler.Stat) bool {
|
||||||
ns1, _ := stats.Max(c1.NScanned)
|
ns1, _ := stats.Max(c1.NScanned)
|
||||||
ns2, _ := stats.Max(c2.NScanned)
|
ns2, _ := stats.Max(c2.NScanned)
|
||||||
return ns1 > ns2
|
return ns1 > ns2
|
||||||
@@ -883,13 +628,13 @@ func sortQueries(queries []stat, orderby []string) []stat {
|
|||||||
|
|
||||||
//
|
//
|
||||||
case "docs-returned":
|
case "docs-returned":
|
||||||
f = func(c1, c2 *stat) bool {
|
f = func(c1, c2 *profiler.Stat) bool {
|
||||||
nr1, _ := stats.Max(c1.NReturned)
|
nr1, _ := stats.Max(c1.NReturned)
|
||||||
nr2, _ := stats.Max(c2.NReturned)
|
nr2, _ := stats.Max(c2.NReturned)
|
||||||
return nr1 < nr2
|
return nr1 < nr2
|
||||||
}
|
}
|
||||||
case "-docs-returned":
|
case "-docs-returned":
|
||||||
f = func(c1, c2 *stat) bool {
|
f = func(c1, c2 *profiler.Stat) bool {
|
||||||
nr1, _ := stats.Max(c1.NReturned)
|
nr1, _ := stats.Max(c1.NReturned)
|
||||||
nr2, _ := stats.Max(c2.NReturned)
|
nr2, _ := stats.Max(c2.NReturned)
|
||||||
return nr1 > nr2
|
return nr1 > nr2
|
||||||
@@ -951,3 +696,17 @@ func isReplicasetEnabled(session pmgo.SessionManager) bool {
|
|||||||
}
|
}
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
BIN
src/go/pt-mongodb-query-digest/pt-mongodb-query-digest
Executable file
BIN
src/go/pt-mongodb-query-digest/pt-mongodb-query-digest
Executable file
Binary file not shown.
Reference in New Issue
Block a user