package main import ( "bytes" "context" "encoding/json" "fmt" "strings" "text/template" "time" "github.com/alecthomas/kong" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "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" ) 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 kong.VersionFlag 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" ) // We do not set anything here, these variables are defined by the Makefile var ( Build string //nolint GoVersion string //nolint Version string //nolint Commit string //nolint ) func main() { var args cmdlineArgs kongctx := kong.Parse(&args, kong.UsageOnError(), kong.Vars{"version": fmt.Sprintf("%s\nVersion %s\nBuild: %s using %s\nCommit: %s", toolname, Version, Build, GoVersion, Commit)}) 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 }