Merge branch 'PT-1978_mdb_summary_index' into release-3.4.0

This commit is contained in:
Carlos Salguero
2022-04-05 09:47:23 -03:00
11 changed files with 636 additions and 24 deletions

33
go.mod
View File

@@ -8,25 +8,28 @@ require (
github.com/go-ini/ini v1.66.4
github.com/golang/mock v1.4.4
github.com/google/uuid v1.3.0
github.com/hashicorp/go-version v1.4.0
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef
github.com/lib/pq v1.10.4
github.com/mattn/go-shellwords v1.0.12
github.com/montanaflynn/stats v0.6.6
github.com/pborman/getopt v1.1.0
github.com/percona/go-mysql v0.0.0-20210427141028-73d29c6da78c
github.com/hashicorp/go-version v1.2.1-0.20190424083514-192140e6f3e6
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
github.com/kr/pretty v0.3.0
github.com/lib/pq v1.2.0
github.com/mattn/go-shellwords v1.0.6
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
github.com/percona/go-mysql v0.0.0-20190903141930-197f4ad8db8d
github.com/pkg/errors v0.9.1
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
go.mongodb.org/mongo-driver v1.8.4
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
github.com/shirou/gopsutil v2.20.8+incompatible
github.com/sirupsen/logrus v1.6.0
github.com/stretchr/testify v1.7.1
go.mongodb.org/mongo-driver v1.7.1
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22
k8s.io/api v0.23.5
)
require (
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
github.com/alecthomas/kong v0.5.0 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -40,9 +43,11 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.1 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.6.1 // indirect
github.com/smartystreets/goconvey v1.7.2 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
@@ -62,7 +67,7 @@ require (
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apimachinery v0.23.5 // indirect
k8s.io/apimachinery v0.18.6 // indirect
k8s.io/klog v1.0.0 // indirect
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect

23
go.sum
View File

@@ -1,4 +1,5 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
@@ -13,15 +14,13 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/kong v0.5.0 h1:u8Kdw+eeml93qtMZ04iei0CFYve/WPcA5IFh+9wSskE=
github.com/alecthomas/kong v0.5.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -178,11 +177,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJ
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -243,6 +243,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/shirou/gopsutil v2.20.8+incompatible h1:8c7Atn0FAUZJo+f4wYbN0iVpdWniCQk7IYwGtgdh1mY=
github.com/shirou/gopsutil v2.20.8+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
@@ -274,6 +276,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
@@ -462,9 +466,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.18.6 h1:osqrAXbOQjkKIWDTjrqxWQ3w0GkKb1KA1XkUGHHYpeE=
k8s.io/api v0.18.6/go.mod h1:eeyxr+cwCjMdLAmr2W3RyDI0VvTawSg/3RFFBEnmZGI=
k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA=

View File

@@ -0,0 +1,48 @@
# pt-mongodb-index-check
## Introduction
This tool can perform checks on MongoDB indexes.
Currently, these checks are available:
### Duplicated indexes
Check for indexes that are the prefix of other indexes. For example if we have these 2 indexes
```
db.getSiblingDB("testdb").test_col.createIndex({"f1": 1, "f2": -1, "f3": 1, "f4": 1}, {"name": "idx_01"});
db.getSiblingDB("testdb").test_col.createIndex({"f1": 1, "f2": -1, "f3": 1}, {"name": "idx_02"});
```
The index `idx_02` is the prefix of `idx_01` because it has the same keys in the same order so, `idx_02` can be dropped.
### Unused indexes.
This check gets the `$indexstats` for all indexes and reports those having `accesses.ops` = 0.
## Usage
Run the program as `pt-mongodb-index-check <command> [flags]`
#### Available commands
| Command | Description |
| ---------------- | ---------------------------------- |
| check-duplicated | Run checks for duplicated indexes. |
| check-unused | Run check for unused indexes. |
| check-all | Run all checks |
#### Available flags
| Flag | Description |
| ---- | ----------- |
|--all-databases|Check in all databases excluding system dbs.|
|--databases=DATABASES,...|Comma separated list of databases to check.|
|--all-collections|Check in all collections in the selected databases.|
|--collections=COLLECTIONS,...|Comma separated list of collections to check.|
|--mongodb.uri=<connection string>|Connection URI|
|--json|Show output as JSON|

View File

@@ -0,0 +1,103 @@
package indexes
import (
"context"
"log"
"sort"
"strings"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type collectionIndex struct {
Name string `bson:"name"`
Namespace string `bson:"ns"`
V int `bson:"v"`
Key primitive.D `bson:"key"`
}
func (di collectionIndex) ComparableKey() string {
str := ""
for _, elem := range di.Key {
str += sign(elem) + elem.Key
}
return str
}
func sign(elem primitive.E) string {
sign := "+"
switch elem.Value.(type) {
case int32: // internal MongoDB indexes like _id_ or lastUsed have the sign field as int32.
if elem.Value.(int32) < 0 {
sign = "-"
}
case float64: // All other indexes have the sign field as float64.
if elem.Value.(float64) < 0 {
sign = "-"
}
}
return sign
}
// IndexKey holds the list of fields that are part of an index, along with the field order.
type IndexKey []primitive.E
// String returns the index fields as a string. The + sign means ascending on this field
// and a - sign indicates a descending order for that field.
func (di IndexKey) String() string {
str := ""
for _, elem := range di {
str += sign(elem) + elem.Key + " "
}
return str
}
// DuplicateIndex represents a duplicated index pair.
// An index is considered as the duplicate of another one if it is it's prefix.
// Example: the index +f1-f2 is the prefix of +f1-f2+f3.
type Duplicate struct {
Namespace string
Name string
Key IndexKey
ContainerName string
ContainerKey IndexKey
}
func FindDuplicated(ctx context.Context, client *mongo.Client, database, collection string) ([]Duplicate, error) {
di := []Duplicate{}
cursor, err := client.Database(database).Collection(collection).Indexes().List(ctx, nil)
if err != nil {
return nil, err
}
var results []collectionIndex
if err = cursor.All(context.TODO(), &results); err != nil {
log.Fatal(err)
}
sort.Slice(results, func(i, j int) bool {
return results[i].ComparableKey() < results[j].ComparableKey()
})
for i := 0; i < len(results)-1; i++ {
for j := i + 1; j < len(results); j++ {
if strings.HasPrefix(results[j].ComparableKey(), results[i].ComparableKey()) {
idx := Duplicate{
Namespace: database + "." + collection,
Name: results[i].Name,
Key: make([]primitive.E, len(results[i].Key)),
ContainerName: results[j].Name,
ContainerKey: make([]primitive.E, len(results[j].Key)),
}
copy(idx.Key, results[i].Key)
copy(idx.ContainerKey, results[j].Key)
di = append(di, idx)
}
}
}
return di, nil
}

View File

@@ -0,0 +1,116 @@
package indexes
import (
"context"
"fmt"
"testing"
"time"
"github.com/AlekSi/pointer"
tu "github.com/percona/percona-toolkit/src/go/internal/testutils"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"gopkg.in/mgo.v2/bson"
)
func TestDuplicateIndexes(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client, err := tu.TestClient(ctx, tu.MongoDBShard1PrimaryPort)
if err != nil {
t.Fatalf("cannot get a new MongoDB client: %s", err)
}
dbname := "test_db"
collname := "test_col"
database := client.Database(dbname)
database.Drop(ctx) //nolint:errcheck
defer database.Drop(ctx) //nolint:errcheck
_, err = database.Collection(collname).InsertOne(ctx, bson.M{"f1": 1, "f2": "2", "f3": "a", "f4": "c"})
assert.NoError(t, err)
testCases := []primitive.D{
{{"f1", 1}, {"f2", -1}, {"f3", 1}, {"f4", 1}},
{{"f1", 1}, {"f2", -1}, {"f3", 1}, {"f4", 1}}, // this will throw a duplicate index error
{{"f1", 1}, {"f2", -1}, {"f3", 1}},
{{"f1", 1}, {"f2", -1}},
{{"f3", -1}},
}
errCount := 0
for i, tc := range testCases {
mod := mongo.IndexModel{
Keys: tc,
Options: &options.IndexOptions{
Name: pointer.ToString(fmt.Sprintf("idx_%02d", i)),
},
}
_, err := database.Collection(collname).Indexes().CreateOne(ctx, mod)
if err != nil {
errCount++
}
}
/*
At this point we have 5 indexes: _id: (MongoDB's default), idx_00, idx_02, idx_03, idx_04.
idx_01 wasn't created since it duplicates idx_00 and errCount=1.
*/
assert.Equal(t, 1, errCount)
want := []Duplicate{
{
Name: "idx_03",
Namespace: "test_db.test_col",
Key: IndexKey{
{Key: "f1", Value: int32(1)},
{Key: "f2", Value: int32(-1)},
},
ContainerName: "idx_02",
ContainerKey: IndexKey{
{Key: "f1", Value: int32(1)},
{Key: "f2", Value: int32(-1)},
{Key: "f3", Value: int32(1)},
},
},
{
Name: "idx_03",
Namespace: "test_db.test_col",
Key: IndexKey{
{Key: "f1", Value: int32(1)},
{Key: "f2", Value: int32(-1)},
},
ContainerName: "idx_00",
ContainerKey: IndexKey{
{Key: "f1", Value: int32(1)},
{Key: "f2", Value: int32(-1)},
{Key: "f3", Value: int32(1)},
{Key: "f4", Value: int32(1)},
},
},
{
Name: "idx_02",
Namespace: "test_db.test_col",
Key: IndexKey{
{Key: "f1", Value: int32(1)},
{Key: "f2", Value: int32(-1)},
{Key: "f3", Value: int32(1)},
},
ContainerName: "idx_00",
ContainerKey: IndexKey{
{Key: "f1", Value: int32(1)},
{Key: "f2", Value: int32(-1)},
{Key: "f3", Value: int32(1)},
{Key: "f4", Value: int32(1)},
},
},
}
di, err := FindDuplicated(ctx, client, dbname, collname)
assert.NoError(t, err)
assert.Equal(t, want, di)
}

View File

@@ -0,0 +1,62 @@
package indexes
import (
"context"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"gopkg.in/mgo.v2/bson"
)
var systemDBs = []string{"admin", "config", "local"} //nolint:gochecknoglobals
// IndexStat hold an index usage statistics.
type IndexStat struct {
Accesses struct {
Ops int64 `bson:"ops"`
Since primitive.DateTime `bson:"since"`
} `bson:"accesses"`
Spec struct {
Name string `bson:"name"`
Namespace string `bson:"ns"`
V int32 `bson:"v"`
Key primitive.D `bson:"key"`
} `bson:"spec"`
Name string `bson:"name"`
Key primitive.D `bson:"key"`
Host string `bson:"host"`
}
func in(search string, items []string) bool {
for _, item := range items {
if search == item {
return true
}
}
return false
}
// FindUnusedIndexes returns a list of unused indexes for the given database and collection.
func FindUnused(ctx context.Context, client *mongo.Client, database, collection string) ([]IndexStat, error) {
aggregation := mongo.Pipeline{
{{Key: "$indexStats", Value: primitive.M{}}},
{{Key: "$match", Value: primitive.M{"accesses.ops": 0}}},
{{Key: "$match", Value: primitive.M{"name": bson.M{"$ne": "_id_"}}}},
}
if in(database, systemDBs) {
return nil, nil
}
cursor, err := client.Database(database).Collection(collection).Aggregate(ctx, aggregation)
if err != nil {
return nil, errors.Wrap(err, "cannot run $indexStats for unused indexes")
}
var stats []IndexStat
if err = cursor.All(ctx, &stats); err != nil {
return nil, errors.Wrap(err, "cannot get $indexStats for unused indexes")
}
return stats, nil
}

View File

@@ -0,0 +1,76 @@
package indexes
import (
"context"
"fmt"
"math/rand"
"sort"
"testing"
"time"
"github.com/AlekSi/pointer"
tu "github.com/percona/percona-toolkit/src/go/internal/testutils"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"gopkg.in/mgo.v2/bson"
)
func TestUnusedIndexes(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client, err := tu.TestClient(ctx, tu.MongoDBShard1PrimaryPort)
if err != nil {
t.Fatalf("cannot get a new MongoDB client: %s", err)
}
dbname := "test_db"
collname := "test_col"
database := client.Database(dbname)
database.Drop(ctx) //nolint:errcheck
defer database.Drop(ctx) //nolint:errcheck
testCases := []primitive.D{
{{"f1", 1}, {"f2", -1}, {"f3", 1}, {"f4", 1}},
{{"f3", -1}},
{{"f4", -1}},
}
errCount := 0
for i, tc := range testCases {
mod := mongo.IndexModel{
Keys: tc,
Options: &options.IndexOptions{
Name: pointer.ToString(fmt.Sprintf("idx_%02d", i)),
},
}
_, err := database.Collection(collname).Indexes().CreateOne(ctx, mod)
if err != nil {
errCount++
}
}
for i := 0; i < 100; i++ {
_, err = database.Collection(collname).InsertOne(ctx,
bson.M{"f1": rand.Int63n(1000), "f2": rand.Int63n(1000), "f3": rand.Int63n(1000), "f4": rand.Int63n(1000)})
assert.NoError(t, err)
}
want := []string{"idx_00", "idx_01", "idx_02"}
ui, err := FindUnused(ctx, client, dbname, collname)
assert.NoError(t, err)
got := make([]string, 0, len(ui))
for _, idx := range ui {
// compare only names because the index struct has a timestamp in it and it is variable.
got = append(got, idx.Name)
}
sort.Strings(got)
assert.Equal(t, want, got)
}

View File

@@ -0,0 +1,163 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"text/template"
"time"
"github.com/alecthomas/kong"
"github.com/percona/percona-toolkit/src/go/pt-mongodb-index-check/indexes"
"github.com/percona/percona-toolkit/src/go/pt-mongodb-index-check/templates"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
log "github.com/sirupsen/logrus"
)
type cmdlineArgs struct {
CheckUnused struct{} `cmd:"" name:"check-unused" help:"Check for unused indexes."`
CheckDuplicated struct{} `cmd:"" name:"check-duplicates" help:"Check for duplicated indexes."`
CheckAll struct{} `cmd:"" name:"check-all" help:"Check for unused and duplicated indexes."`
ShowHelp struct{} `cmd:"" default:"1"`
AllDatabases bool `name:"all-databases" xor:"db" help:"Check in all databases excluding system dbs"`
Databases []string `name:"databases" xor:"db" help:"Comma separated list of databases to check"`
AllCollections bool `name:"all-collections" xor:"colls" help:"Check in all collections in the selected databases."`
Collections []string `name:"collections" xor:"colls" help:"Comma separated list of collections to check"`
URI string `name:"mongodb.uri" help:"Connection URI"`
JSON bool `name:"json" help:"Show output as JSON"`
}
type response struct {
Unused []indexes.IndexStat
Duplicated []indexes.Duplicate
}
func main() {
var args cmdlineArgs
kongctx := kong.Parse(&args, kong.UsageOnError())
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(args.URI))
if err != nil {
log.Fatalf("Cannot connect to the database: %q", err)
}
if args.AllDatabases {
args.Databases, err = client.ListDatabaseNames(context.TODO(), primitive.D{})
if err != nil {
log.Fatalf("cannot list all databases: %s", err)
}
}
if args.AllCollections {
args.Collections = nil
}
resp := response{}
switch kongctx.Command() {
case "check-unused":
resp.Unused = append(resp.Unused, findUnused(ctx, client, args.Databases, args.Collections)...)
case "check-duplicates":
resp.Duplicated = append(resp.Duplicated, findDuplicated(ctx, client, args.Databases, args.Collections)...)
case "check-all":
resp.Unused = append(resp.Unused, findUnused(ctx, client, args.Databases, args.Collections)...)
resp.Duplicated = append(resp.Duplicated, findDuplicated(ctx, client, args.Databases, args.Collections)...)
default:
kong.DefaultHelpPrinter(kong.HelpOptions{}, kongctx)
}
fmt.Println(output(resp, args.JSON))
}
func output(resp response, asJson bool) string {
if asJson {
jsonStr, err := json.MarshalIndent(resp, "", "\t")
if err != nil {
log.Fatal("cannot encode the response as json")
}
return string(jsonStr)
}
buf := new(bytes.Buffer)
t := template.Must(template.New("duplicated").Parse(templates.Duplicated))
if err := t.Execute(buf, resp.Duplicated); err != nil {
log.Fatal(errors.Wrap(err, "cannot parse clusterwide section of the output template"))
}
t = template.Must(template.New("unused").Parse(templates.Unused))
if err := t.Execute(buf, resp.Unused); err != nil {
log.Fatal(errors.Wrap(err, "cannot parse clusterwide section of the output template"))
}
return buf.String()
}
func findUnused(ctx context.Context, client *mongo.Client, databases []string, collections []string) []indexes.IndexStat {
unused := []indexes.IndexStat{}
var err error
colls := make([]string, len(collections))
copy(colls, collections)
for _, database := range databases {
if len(collections) == 0 {
colls, err = client.Database(database).ListCollectionNames(ctx, primitive.D{})
if err != nil {
log.Errorf("cannot get the list of collections for the database %s", database)
continue
}
}
for _, collection := range colls {
idx, err := indexes.FindUnused(ctx, client, database, collection)
if err != nil {
log.Errorf("error while checking unused indexes in %s.%s: %s", database, collection, err)
continue
}
unused = append(unused, idx...)
}
}
return unused
}
func findDuplicated(ctx context.Context, client *mongo.Client, databases []string, collections []string) []indexes.Duplicate {
duplicated := []indexes.Duplicate{}
var err error
colls := make([]string, len(collections))
copy(colls, collections)
for _, database := range databases {
if len(collections) == 0 {
colls, err = client.Database(database).ListCollectionNames(ctx, primitive.D{})
if err != nil {
log.Errorf("cannot get the list of collections for the database %s", database)
continue
}
}
for _, collection := range colls {
dups, err := indexes.FindDuplicated(ctx, client, database, collection)
if err != nil {
log.Errorf("error while checking duplicated indexes in %s.%s: %s", database, collection, err)
continue
}
duplicated = append(duplicated, dups...)
}
}
return duplicated
}

View File

@@ -0,0 +1,10 @@
package templates
// {{if $i}},{{end}} adds a comma after the first element.
// When $i == 0 (first element) {{ if $i }} returns false (0)
var Duplicated = `
Duplicated indexes
{{ range . }}
{{ .Namespace }}, index '{{ .Name }}', with fields { {{- range $i, $val := .Key }}{{if $i}}, {{end}}{{ $val.Key }}:{{ $val.Value }}{{ end -}} } is the prefix of '{{ .ContainerName }}' with fields { {{- range $i, $val := .ContainerKey }}{{if $i}}, {{end}}{{ $val.Key }}:{{ $val.Value }}{{ end -}} }{{ end}}
`

View File

@@ -0,0 +1,10 @@
package templates
// {{if $i}},{{end}} adds a comma after the first element.
// When $i == 0 (first element) {{ if $i }} returns false (0)
var Unused = `
Unused indexes
{{ range . }}
{{ .Spec.Namespace }}, index '{{ .Name }}' with fields { {{- range $i, $val := .Key }}{{if $i}}, {{end}}{{ $val.Key }}:{{ $val.Value }} }{{ end }}{{ end}}
`

View File

@@ -0,0 +1,16 @@
package templates
const Security = `
# Security ###############################################################################################
Users : {{.Users}}
Roles : {{.Roles}}
Auth : {{.Auth}}
SSL : {{.SSL}}
Port : {{.Port}}
Bind IP: {{.BindIP}}
{{- if .WarningMsgs -}}
{{- range .WarningMsgs }}
{{ . }}
{{end}}
{{end }}
`