func (ix *IndexStats) OutputJSON() error {
	var err error
	var data []byte
	if data, err = bson.MarshalExtJSON(ix, false, false); err != nil {
		return err
	}
	os.Mkdir(outdir, 0755)
	ofile := outdir + strings.ReplaceAll(filepath.Base(ix.filename), "bson.gz", "json")
	ioutil.WriteFile(ofile, data, 0644)
	fmt.Println("json data written to", ofile)
	return err
}
OutputJSON writes json data to a file
package mdb
import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
	"github.com/simagix/gox"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)
type IndexStats struct {
	Databases []Database  `bson:"databases"`
	Logger    *gox.Logger `bson:"keyhole"`
	filename string
	nocolor  bool
	verbose  bool
	version  string
}
type Accesses struct {
	Ops   int       `json:"ops" bson:"ops"`
	Since time.Time `json:"since" bson:"since"`
}
type IndexUsage struct {
	Accesses Accesses `json:"accesses" bson:"accesses"`
	Host     string   `json:"host" bson:"host"`
	Name     string   `json:"name" bson:"name"`
	Shard    string   `json:"shard" bson:"shard"`
}
type Index struct {
	Background              bool   `json:"background" bson:"background"`
	Collation               bson.D `json:"collation" bson:"collation"`
	ExpireAfterSeconds      int32  `json:"expireAfterSeconds" bson:"expireAfterSeconds,truncate,omitempty"`
	Key                     bson.D `json:"key" bson:"key"`
	Name                    string `json:"name" bson:"name,truncate"`
	PartialFilterExpression bson.D `json:"partialFilterExpression" bson:"partialFilterExpression"`
	Sparse                  bool   `json:"sparse" bson:"sparse"`
	Unique                  bool   `json:"unique" bson:"unique"`
	Version                 int32  `json:"v" bson:"v,truncate"`
	Weights                 bson.D `json:"weights" bson:"weights"`
	EffectiveKey string       `json:"effectiveKey" bson:"effectiveKey"`
	Fields       []string     `json:"fields" bson:"fields"`
	IsDupped     bool         `json:"isDupped" bson:"isDupped"`
	IsShardKey   bool         `json:"isShardkey" bson:"isShardkey"`
	KeyString    string       `json:"keyString" bson:"keyString"`
	TotalOps     int          `json:"totalOps" bson:"totalOps"`
	Usage        []IndexUsage `json:"usage" bson:"usage"`
}
const (
	indexExt = "-index.bson.gz"
)
func NewIndexStats(version string) *IndexStats {
	hostname, _ := os.Hostname()
	return &IndexStats{version: version, Logger: gox.GetLogger(version),
		filename: hostname + indexExt, Databases: []Database{}}
}
func (ix *IndexStats) SetFilename(filename string) {
	ix.filename = strings.Replace(filename, ":", "_", -1)
}
func (ix *IndexStats) SetClusterDetailsFromFile(filename string) error {
	if !strings.HasSuffix(filename, indexExt) && !strings.HasSuffix(filename, "-stats.bson.gz") {
		return errors.New("unsupported file type")
	}
	var data []byte
	var err error
	var fd *bufio.Reader
	if fd, err = gox.NewFileReader(filename); err != nil {
		return err
	}
	if data, err = ioutil.ReadAll(fd); err != nil {
		return err
	}
	return bson.Unmarshal(data, &ix)
}
func (ix *IndexStats) SetNoColor(nocolor bool) {
	ix.nocolor = nocolor
}
func (ix *IndexStats) SetVerbose(verbose bool) {
	ix.verbose = verbose
	if verbose && ix.Logger != nil {
		ix.Logger.SetLoggerLevel(gox.Debug)
	}
}
func (ix *IndexStats) GetIndexes(client *mongo.Client) ([]Database, error) {
	var err error
	var dbNames []string
	var collections []Collection
	ix.Databases = []Database{}
	var databases []Database
	if dbNames, err = GetDatabaseNames(client); err != nil {
		return databases, err
	}
	cnt := 0
	for _, name := range dbNames {
		if name == "admin" || name == "config" || name == "local" {
			ix.Logger.Debug("skip ", name)
			continue
		}
		cnt++
		ix.Logger.Debug("checking ", name)
		if collections, err = ix.GetIndexesFromDB(client, name); err != nil {
			return ix.Databases, err
		}
		ix.Databases = append(ix.Databases, Database{Name: name, Collections: collections})
	}
	if cnt == 0 && ix.verbose {
		ix.Logger.Info("No database is available")
	}
	ix.Logger.Info(`GetIndexes ends`)
	return ix.Databases, err
}
func (ix *IndexStats) GetIndexesFromDB(client *mongo.Client, db string) ([]Collection, error) {
	var err error
	var cur *mongo.Cursor
	var ctx = context.Background()
	var collections []Collection
	ix.Logger.Debugf(`GetIndexesFromDB(%v)`, db)
	if cur, err = client.Database(db).ListCollections(ctx, bson.M{}); err != nil {
		return collections, err
	}
	defer cur.Close(ctx)
	collectionNames := []string{}
	for cur.Next(ctx) {
		var elem struct {
			Name string `bson:"name"`
			Type string `bson:"type"`
		}
		if err = cur.Decode(&elem); err != nil {
			continue
		}
		if strings.HasPrefix(elem.Name, "system.") || (elem.Type != "" && elem.Type != "collection") {
			ix.Logger.Debug("skip ", elem.Name)
			continue
		}
		collectionNames = append(collectionNames, elem.Name)
	}
	sort.Strings(collectionNames)
	for _, v := range collectionNames {
		var collection = Collection{NS: db + "." + v, Name: v}
		if collection.Indexes, err = ix.GetIndexesFromCollection(client, client.Database(db).Collection(v)); err != nil {
			return collections, err
		}
		collections = append(collections, collection)
	}
	return collections, nil
}
func (ix *IndexStats) GetIndexesFromCollection(client *mongo.Client, collection *mongo.Collection) ([]Index, error) {
	var err error
	var ctx = context.Background()
	var pipeline = MongoPipeline(`{"$indexStats": {}}`)
	var list []Index
	var icur *mongo.Cursor
	var scur *mongo.Cursor
	db := collection.Database().Name()
	ix.Logger.Debugf(`GetIndexesFromCollection from %v.%v`, db, collection.Name())
	var indexStats = []IndexUsage{}
	if scur, err = collection.Aggregate(ctx, pipeline); err != nil {
		ix.Logger.Error(err)
	} else {
		for scur.Next(ctx) {
			var result IndexUsage
			if err = scur.Decode(&result); err != nil {
				ix.Logger.Error(err)
				continue
			}
			indexStats = append(indexStats, result)
		}
		scur.Close(ctx)
	}
	cmd := bson.D{{Key: "listIndexes", Value: collection.Name()}}
	if icur, err = client.Database(db).RunCommandCursor(ctx, cmd); err != nil {
		ix.Logger.Error(err)
		return list, err
	}
	defer icur.Close(ctx)
	indexesFound := map[int]bool{}
	for icur.Next(ctx) {
		o := Index{ExpireAfterSeconds: -1}
		if err = icur.Decode(&o); err != nil {
			ix.Logger.Error(err)
			continue
		}
		var strbuf bytes.Buffer
		fields := []string{}
		for n, value := range o.Key {
			fields = append(fields, value.Key)
			if n == 0 {
				strbuf.WriteString("{ ")
			}
			strbuf.WriteString(value.Key + ": " + fmt.Sprint(value.Value))
			if n == len(o.Key)-1 {
				strbuf.WriteString(" }")
			} else {
				strbuf.WriteString(", ")
			}
		}
		o.Fields = fields
		o.KeyString = strbuf.String()
		var v map[string]interface{}
		ns := collection.Database().Name() + "." + collection.Name()
		ix.Logger.Debug("GetIndexesFromCollection ", ns, o.KeyString)
		if err = client.Database("config").Collection("collections").FindOne(ctx, bson.M{"_id": ns, "key": o.Key}).Decode(&v); err == nil {
			o.IsShardKey = true
		}
		o.EffectiveKey = strings.Replace(o.KeyString[2:len(o.KeyString)-2], ": -1", ": 1", -1)
		o.Usage = []IndexUsage{}
		for i, result := range indexStats {
			if result.Name == o.Name {
				indexesFound[i] = true
				o.TotalOps += result.Accesses.Ops
				o.Usage = append(o.Usage, result)
			}
		}
		list = append(list, o)
	}
	sort.Slice(list, func(i, j int) bool { return (list[i].EffectiveKey < list[j].EffectiveKey) })
	for i, o := range list {
		if o.KeyString != "{ _id: 1 }" && !o.IsShardKey {
			list[i].IsDupped = checkIfDupped(o, list)
		}
	}
	if len(indexesFound) != len(indexStats) {
		for i := 0; i < len(indexStats); i++ {
			if !indexesFound[i] {
				ns := collection.Database().Name() + "." + collection.Name()
				ix.Logger.Warnf(`inconsistent index '%v' of namespace '%v' on shard '%v'`,
					indexStats[i].Name, ns, indexStats[i].Shard)
			}
		}
	}
	return list, nil
}
func checkIfDupped(doc Index, list []Index) bool {
	if strings.Index(doc.KeyString, "2dsphere") > 0 {
		return false
	}
	for _, o := range list {
		if strings.Index(o.KeyString, "2dsphere") > 0 {
			continue
		} else if !o.IsDupped && doc.Fields[0] == o.Fields[0] && doc.KeyString != o.KeyString && len(o.Fields) >= len(doc.Fields) {
			nmatched := 0
			for i, fld := range doc.Fields {
				if i == 0 {
					continue
				}
				for j, field := range o.Fields {
					if j > 0 && fld == field {
						nmatched++
						break
					}
				}
			}
			if nmatched == len(doc.Fields)-1 {
				return true
			}
		}
	}
	return false
}
func (ix *IndexStats) OutputBSON() (string, []byte, error) {
	var err error
	var bsond bson.D
	var data []byte
	var ofile string
	if data, err = bson.Marshal(ix); err != nil {
		return ofile, data, err
	}
	bson.Unmarshal(data, &bsond)
	if data, err = bson.Marshal(bsond); err != nil {
		return ofile, data, err
	}
	os.Mkdir(outdir, 0755)
	idx := strings.Index(ix.filename, indexExt)
	basename := ix.filename[:idx]
	ofile = fmt.Sprintf(`%v/%v%v`, outdir, basename, indexExt)
	i := 1
	for DoesFileExist(ofile) {
		ofile = fmt.Sprintf(`%v/%v.%d%v`, outdir, basename, i, indexExt)
		i++
	}
	if err = gox.OutputGzipped(data, ofile); err == nil {
		fmt.Println("Index stats is written to", ofile)
	}
	return ofile, data, err
}