diff --git a/go.mod b/go.mod index 64f01905..9b85f793 100644 --- a/go.mod +++ b/go.mod @@ -40,9 +40,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 diff --git a/go.sum b/go.sum index 7247e7bc..70c1e32e 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,9 @@ 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= @@ -242,6 +245,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= diff --git a/src/go/pt-mongodb-index-check/README.md b/src/go/pt-mongodb-index-check/README.md new file mode 100644 index 00000000..e4dae670 --- /dev/null +++ b/src/go/pt-mongodb-index-check/README.md @@ -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 [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 URI| +|--json|Show output as JSON| + diff --git a/src/go/pt-mongodb-index-check/indexes/duplicated.go b/src/go/pt-mongodb-index-check/indexes/duplicated.go new file mode 100644 index 00000000..89742e22 --- /dev/null +++ b/src/go/pt-mongodb-index-check/indexes/duplicated.go @@ -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 +} diff --git a/src/go/pt-mongodb-index-check/indexes/duplicated_test.go b/src/go/pt-mongodb-index-check/indexes/duplicated_test.go new file mode 100644 index 00000000..708d7f02 --- /dev/null +++ b/src/go/pt-mongodb-index-check/indexes/duplicated_test.go @@ -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) +} diff --git a/src/go/pt-mongodb-index-check/indexes/unused.go b/src/go/pt-mongodb-index-check/indexes/unused.go new file mode 100644 index 00000000..c506b8d1 --- /dev/null +++ b/src/go/pt-mongodb-index-check/indexes/unused.go @@ -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", "system.profile"} //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 +} diff --git a/src/go/pt-mongodb-index-check/indexes/unused_test.go b/src/go/pt-mongodb-index-check/indexes/unused_test.go new file mode 100644 index 00000000..d0a08879 --- /dev/null +++ b/src/go/pt-mongodb-index-check/indexes/unused_test.go @@ -0,0 +1,81 @@ +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) + } + + // Make use of idx_02: {"f4": -1} to exclude it from the results so we ensure only unused indexes + // are being listed. + _, err = database.Collection(collname).Find(ctx, primitive.M{"f4": primitive.M{"$gt": 500}}) + assert.NoError(t, err) + + want := []string{"idx_00", "idx_01"} + + 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) +} diff --git a/src/go/pt-mongodb-index-check/main.go b/src/go/pt-mongodb-index-check/main.go new file mode 100644 index 00000000..ccaa2cbe --- /dev/null +++ b/src/go/pt-mongodb-index-check/main.go @@ -0,0 +1,189 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + "text/template" + "time" + + "github.com/alecthomas/kong" + "github.com/pkg/errors" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + + "github.com/percona/percona-toolkit/src/go/pt-mongodb-index-check/indexes" + "github.com/percona/percona-toolkit/src/go/pt-mongodb-index-check/templates" + + 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"` + Version struct{} `cmd:"" name:"version"` + + 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" required:"" placeholder:"mongodb://host:port/admindb?options" help:"Connection URI"` + JSON bool `name:"json" help:"Show output as JSON"` +} + +type response struct { + Unused []indexes.IndexStat + Duplicated []indexes.Duplicate +} + +const ( + TOOLNAME = "pt-mongodb-index-check" +) + +var ( + Build string = "2020-04-23" //nolint + GoVersion string = "1.14.1" //nolint + Version string = "3.4.0" //nolint + Commit string //nolint +) + +func main() { + var args cmdlineArgs + kongctx := kong.Parse(&args, kong.UsageOnError()) + + if kongctx.Command() == "version" { + fmt.Println(TOOLNAME) + fmt.Printf("Version %s\n", Version) + fmt.Printf("Build: %s using %s\n", Build, GoVersion) + fmt.Printf("Commit: %s\n", Commit) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + if !strings.HasPrefix(args.URI, "mongodb") && !strings.HasPrefix(args.URI, "mongodb+srv") { + args.URI = "mongodb://" + args.URI + } + + 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 = findUnused(ctx, client, args.Databases, args.Collections) + case "check-duplicates": + resp.Duplicated = findDuplicated(ctx, client, args.Databases, args.Collections) + case "check-all": + resp.Unused = findUnused(ctx, client, args.Databases, args.Collections) + 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 +} diff --git a/src/go/pt-mongodb-index-check/templates/duplicated.go b/src/go/pt-mongodb-index-check/templates/duplicated.go new file mode 100644 index 00000000..2b2d1875 --- /dev/null +++ b/src/go/pt-mongodb-index-check/templates/duplicated.go @@ -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}} +` diff --git a/src/go/pt-mongodb-index-check/templates/unused.go b/src/go/pt-mongodb-index-check/templates/unused.go new file mode 100644 index 00000000..48a0f3bd --- /dev/null +++ b/src/go/pt-mongodb-index-check/templates/unused.go @@ -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 since last restart +{{ range . }} +{{ .Spec.Namespace }}, index '{{ .Name }}' with fields { {{- range $i, $val := .Key }}{{if $i}}, {{end}}{{ $val.Key }}:{{ $val.Value }} }{{ end }}{{ end}} +`