diff options
author | Sam Helman <sam.helman@10gen.com> | 2014-10-10 13:13:09 -0400 |
---|---|---|
committer | Sam Helman <sam.helman@10gen.com> | 2014-10-14 14:08:23 -0400 |
commit | 0bc84a9c2194c2ee820a334069e84645a84418e3 (patch) | |
tree | d7568597680b251f390aae57bdc45820700c36c9 /mongotop | |
parent | 823fb2982e2f3f4a3c26af5681941224bb6882bb (diff) | |
download | mongo-0bc84a9c2194c2ee820a334069e84645a84418e3.tar.gz |
TOOLS-274: vendor dependencies
Former-commit-id: 3022e68e9a6d1e684364591804671a06c64a27ab
Diffstat (limited to 'mongotop')
-rw-r--r-- | mongotop/command/command.go | 20 | ||||
-rw-r--r-- | mongotop/command/serverstatus.go | 94 | ||||
-rw-r--r-- | mongotop/command/serverstatus_test.go | 28 | ||||
-rw-r--r-- | mongotop/command/top.go | 121 | ||||
-rw-r--r-- | mongotop/command/top_test.go | 258 | ||||
-rw-r--r-- | mongotop/main/mongotop.go | 74 | ||||
-rw-r--r-- | mongotop/mongotop.go | 106 | ||||
-rw-r--r-- | mongotop/options/options.go | 22 | ||||
-rw-r--r-- | mongotop/output/outputter.go | 51 | ||||
-rwxr-xr-x | mongotop/smoke.sh | 33 |
10 files changed, 807 insertions, 0 deletions
diff --git a/mongotop/command/command.go b/mongotop/command/command.go new file mode 100644 index 00000000000..ef6ec5161cb --- /dev/null +++ b/mongotop/command/command.go @@ -0,0 +1,20 @@ +package command + +// Interface for a single command that can be run against a MongoDB connection. +type Command interface { + + // Convert to an interface that can be passed to the Run() method of + // a mgo.DB instance + AsRunnable() interface{} + + // Diff the Command against another Command + Diff(Command) (Diff, error) +} + +// Interface for a diff between the results of two commands run against the +// database. +type Diff interface { + + // Convert to rows, to be printed easily. + ToRows() [][]string +} diff --git a/mongotop/command/serverstatus.go b/mongotop/command/serverstatus.go new file mode 100644 index 00000000000..df20f8fa289 --- /dev/null +++ b/mongotop/command/serverstatus.go @@ -0,0 +1,94 @@ +package command + +import ( + "fmt" + "github.com/mongodb/mongo-tools/common/util" + "strconv" +) + +// Struct implementing the Command interface for the serverStatus command. +type ServerStatus struct { + Locks map[string]NSLocksInfo `bson:"locks"` +} + +// Subfield of the serverStatus command. +type NSLocksInfo struct { + TimeLockedMicros map[string]int `bson:"timeLockedMicros"` +} + +// Implements the Diff interface for the diff between two serverStatus commands. +type ServerStatusDiff struct { + // namespace - > totals + Totals map[string][]int +} + +// Implement the Diff interface. Serializes the lock totals into rows by +// namespace. +func (self *ServerStatusDiff) ToRows() [][]string { + // to return + rows := [][]string{} + + // the header row + headerRow := []string{"db", "total", "read", "write"} + rows = append(rows, headerRow) + + // create the rows for the individual namespaces + for ns, nsTotals := range self.Totals { + + nsRow := []string{ns} + for _, total := range nsTotals { + nsRow = append(nsRow, strconv.Itoa(util.MaxInt(0, total/1000))+"ms") + } + rows = append(rows, nsRow) + + } + + return rows +} + +// Needed to implement the common/db/command's Command interface, in order to +// be run as a command against the database. +func (self *ServerStatus) AsRunnable() interface{} { + return "serverStatus" +} + +// Needed to implement the local package's Command interface. Diffs the server +// status result against another server status result. +func (self *ServerStatus) Diff(other Command) (Diff, error) { + + // the diff to eventually return + diff := &ServerStatusDiff{ + Totals: map[string][]int{}, + } + + var otherAsServerStatus *ServerStatus + var ok bool + if otherAsServerStatus, ok = other.(*ServerStatus); !ok { + return nil, fmt.Errorf("a *ServerStatus can only diff against another" + + " *ServerStatus") + } + + firstLocks := otherAsServerStatus.Locks + secondLocks := self.Locks + for ns, firstNSInfo := range firstLocks { + if secondNSInfo, ok := secondLocks[ns]; ok { + + firstTimeLocked := firstNSInfo.TimeLockedMicros + secondTimeLocked := secondNSInfo.TimeLockedMicros + + diff.Totals[ns] = []int{ + (secondTimeLocked["r"] + secondTimeLocked["R"]) - + (firstTimeLocked["r"] + firstTimeLocked["R"]), + (secondTimeLocked["w"] + secondTimeLocked["W"]) - + (firstTimeLocked["w"] + firstTimeLocked["W"]), + } + + diff.Totals[ns] = append( + []int{diff.Totals[ns][0] + diff.Totals[ns][1]}, + diff.Totals[ns]..., + ) + } + } + + return diff, nil +} diff --git a/mongotop/command/serverstatus_test.go b/mongotop/command/serverstatus_test.go new file mode 100644 index 00000000000..0c2d008c279 --- /dev/null +++ b/mongotop/command/serverstatus_test.go @@ -0,0 +1,28 @@ +package command + +import ( + "github.com/mongodb/mongo-tools/common/testutil" + . "github.com/smartystreets/goconvey/convey" + "testing" +) + +func TestServerStatusCommandDiff(t *testing.T) { + + testutil.VerifyTestType(t, "unit") + + Convey("When diffing two ServerStatus commands", t, func() { + + }) + +} + +func TestServerStatusDiffToRows(t *testing.T) { + + testutil.VerifyTestType(t, "unit") + + Convey("When converting a ServerStatusDiff to rows to be "+ + " printed", t, func() { + + }) + +} diff --git a/mongotop/command/top.go b/mongotop/command/top.go new file mode 100644 index 00000000000..0c2e0870e90 --- /dev/null +++ b/mongotop/command/top.go @@ -0,0 +1,121 @@ +package command + +import ( + "fmt" + "github.com/mongodb/mongo-tools/common/util" + "sort" + "strconv" + "strings" + "time" +) + +type Top struct { + // namespace -> namespace-specific top info + Totals map[string]NSTopInfo `bson:"totals"` +} + +// Info within the top command about a single namespace. +type NSTopInfo struct { + Total TopField `bson:"total"` + Read TopField `bson:"readLock"` + Write TopField `bson:"writeLock"` +} + +// Top information about a single field in a namespace. +type TopField struct { + Time int `bson:"time"` + Count int `bson:"count"` +} + +// Struct representing the diff between two top command results. +type TopDiff struct { + // namespace -> totals + Totals map[string][]int +} + +// Implement the Diff interface. Serializes the information about the time +// spent in locks into rows to be printed. +func (self *TopDiff) ToRows() [][]string { + // to return + rows := [][]string{} + + // the header row + headerRow := []string{"ns", "total", "read", "write", + time.Now().Format("2006-01-02T15:04:05")} + rows = append(rows, headerRow) + + // sort the namespaces + nsSorted := []string{} + for ns := range self.Totals { + nsSorted = append(nsSorted, ns) + } + sort.Strings(nsSorted) + + // create the rows for the individual namespaces, iterating in sorted + // order + for _, ns := range nsSorted { + + nsTotals := self.Totals[ns] + + // some namespaces are skipped + if skipNamespace(ns) { + continue + } + + nsRow := []string{ns} + for _, total := range nsTotals { + nsRow = append(nsRow, strconv.Itoa(util.MaxInt(0, total/1000))+"ms") + } + rows = append(rows, nsRow) + } + + return rows +} + +// Determines whether or not a namespace should be skipped for the purposes +// of printing the top results. +func skipNamespace(ns string) bool { + return ns == "" || + !strings.Contains(ns, ".") || + strings.HasSuffix(ns, "namespaces") || + strings.HasPrefix(ns, "local") +} + +// Implement the common/db/command package's Command interface, in order to be +// run as a database command. +func (self *Top) AsRunnable() interface{} { + return "top" +} + +// Implement the local package's Command interface, for the purposes of +// computing and outputting diffs. +func (self *Top) Diff(other Command) (Diff, error) { + + // the diff to eventually return + diff := &TopDiff{ + Totals: map[string][]int{}, + } + + // make sure the other command to be diffed against is of the same type + var otherAsTop *Top + var ok bool + if otherAsTop, ok = other.(*Top); !ok { + return nil, fmt.Errorf("a *Top can only diff against another *Top") + } + + // create the fields for each namespace existing in both the old and + // new results + firstTotals := otherAsTop.Totals + secondTotals := self.Totals + for ns, firstNSInfo := range firstTotals { + if secondNSInfo, ok := secondTotals[ns]; ok { + diff.Totals[ns] = []int{ + secondNSInfo.Total.Time - firstNSInfo.Total.Time, + secondNSInfo.Read.Time - firstNSInfo.Total.Time, + secondNSInfo.Write.Time - firstNSInfo.Total.Time, + } + } + } + + return diff, nil +} diff --git a/mongotop/command/top_test.go b/mongotop/command/top_test.go new file mode 100644 index 00000000000..447800b13f6 --- /dev/null +++ b/mongotop/command/top_test.go @@ -0,0 +1,258 @@ +package command + +import ( + "github.com/mongodb/mongo-tools/common/testutil" + . "github.com/smartystreets/goconvey/convey" + "testing" +) + +func TestTopDiff(t *testing.T) { + + testutil.VerifyTestType(t, "unit") + + Convey("When diffing two Top commands", t, func() { + + var firstTop *Top + var secondTop *Top + + Convey("any namespaces only existing in the first Top should be"+ + " ignored", func() { + + firstTop = &Top{ + Totals: map[string]NSTopInfo{ + "a": NSTopInfo{}, + "b": NSTopInfo{}, + "c": NSTopInfo{}, + }, + } + + secondTop = &Top{ + Totals: map[string]NSTopInfo{ + "a": NSTopInfo{}, + }, + } + + diff, err := secondTop.Diff(firstTop) + So(err, ShouldBeNil) + + asTopDiff, ok := diff.(*TopDiff) + So(ok, ShouldBeTrue) + + _, hasA := asTopDiff.Totals["a"] + So(hasA, ShouldBeTrue) + _, hasB := asTopDiff.Totals["b"] + So(hasB, ShouldBeFalse) + _, hasC := asTopDiff.Totals["c"] + So(hasC, ShouldBeFalse) + + }) + + Convey("any namespaces only existing in the second Top should be"+ + " ignored", func() { + + firstTop = &Top{ + Totals: map[string]NSTopInfo{ + "a": NSTopInfo{}, + }, + } + + secondTop = &Top{ + Totals: map[string]NSTopInfo{ + "a": NSTopInfo{}, + "b": NSTopInfo{}, + "c": NSTopInfo{}, + }, + } + + diff, err := secondTop.Diff(firstTop) + So(err, ShouldBeNil) + + asTopDiff, ok := diff.(*TopDiff) + So(ok, ShouldBeTrue) + + _, hasA := asTopDiff.Totals["a"] + So(hasA, ShouldBeTrue) + _, hasB := asTopDiff.Totals["b"] + So(hasB, ShouldBeFalse) + _, hasC := asTopDiff.Totals["c"] + So(hasC, ShouldBeFalse) + + }) + + Convey("the differences for the times for any shared namespaces should"+ + "be included in the output", func() { + + firstTop = &Top{ + Totals: map[string]NSTopInfo{ + "a": NSTopInfo{ + Total: TopField{ + Time: 2, + }, + Read: TopField{ + Time: 2, + }, + Write: TopField{ + Time: 2, + }, + }, + "b": NSTopInfo{ + Total: TopField{ + Time: 2, + }, + Read: TopField{ + Time: 2, + }, + Write: TopField{ + Time: 2, + }, + }, + }, + } + + secondTop = &Top{ + Totals: map[string]NSTopInfo{ + "a": NSTopInfo{ + Total: TopField{ + Time: 3, + }, + Read: TopField{ + Time: 3, + }, + Write: TopField{ + Time: 3, + }, + }, + "b": NSTopInfo{ + Total: TopField{ + Time: 4, + }, + Read: TopField{ + Time: 4, + }, + Write: TopField{ + Time: 4, + }, + }, + }, + } + + diff, err := secondTop.Diff(firstTop) + So(err, ShouldBeNil) + + asTopDiff, ok := diff.(*TopDiff) + So(ok, ShouldBeTrue) + + So(asTopDiff.Totals["a"], ShouldResemble, []int{1, 1, 1}) + So(asTopDiff.Totals["b"], ShouldResemble, []int{2, 2, 2}) + + }) + + }) + +} + +func TestTopDiffToRows(t *testing.T) { + + testutil.VerifyTestType(t, "unit") + + Convey("When converting a TopDiff to rows to be printed", t, func() { + + var diff *TopDiff + + Convey("the first row should contain the appropriate column"+ + " headers", func() { + + diff = &TopDiff{ + Totals: map[string][]int{}, + } + + rows := diff.ToRows() + So(len(rows), ShouldEqual, 1) + headerRow := rows[0] + So(len(headerRow), ShouldEqual, 5) + So(headerRow[0], ShouldEqual, "ns") + So(headerRow[1], ShouldEqual, "total") + So(headerRow[2], ShouldEqual, "read") + So(headerRow[3], ShouldEqual, "write") + + }) + + Convey("the subsequent rows should contain the correct totals from"+ + " the diff", func() { + + diff = &TopDiff{ + Totals: map[string][]int{ + "a.b": []int{0, 1000, 2000}, + "c.d": []int{2000, 1000, 0}, + }, + } + + rows := diff.ToRows() + So(len(rows), ShouldEqual, 3) + So(rows[1][0], ShouldEqual, "a.b") + So(rows[1][1], ShouldEqual, "0ms") + So(rows[1][2], ShouldEqual, "1ms") + So(rows[1][3], ShouldEqual, "2ms") + So(rows[2][0], ShouldEqual, "c.d") + So(rows[2][1], ShouldEqual, "2ms") + So(rows[2][2], ShouldEqual, "1ms") + So(rows[2][3], ShouldEqual, "0ms") + + }) + + Convey("the namespaces should appear in alphabetical order", func() { + + diff = &TopDiff{ + Totals: map[string][]int{ + "a.b": []int{}, + "a.c": []int{}, + "a.a": []int{}, + }, + } + + rows := diff.ToRows() + So(len(rows), ShouldEqual, 4) + So(rows[1][0], ShouldEqual, "a.a") + So(rows[2][0], ShouldEqual, "a.b") + So(rows[3][0], ShouldEqual, "a.c") + + }) + + Convey("any negative values should be capped to 0", func() { + + diff = &TopDiff{ + Totals: map[string][]int{ + "a.b": []int{-1000, 5000, -3000}, + }, + } + + rows := diff.ToRows() + So(len(rows), ShouldEqual, 2) + So(rows[1][1], ShouldEqual, "0ms") + So(rows[1][2], ShouldEqual, "5ms") + So(rows[1][3], ShouldEqual, "0ms") + + }) + + Convey("any namespaces that are just a database, are from the local"+ + "database, or are a collection of namespaces should be"+ + " skipped", func() { + + diff := &TopDiff{ + Totals: map[string][]int{ + "a.b": []int{}, + "local.b": []int{}, + "a.namespaces": []int{}, + "a": []int{}, + }, + } + + rows := diff.ToRows() + So(len(rows), ShouldEqual, 2) + So(rows[1][0], ShouldEqual, "a.b") + + }) + + }) + +} diff --git a/mongotop/main/mongotop.go b/mongotop/main/mongotop.go new file mode 100644 index 00000000000..e1e5406fba5 --- /dev/null +++ b/mongotop/main/mongotop.go @@ -0,0 +1,74 @@ +// Main package for the mongotop tool. +package main + +import ( + "github.com/mongodb/mongo-tools/common/db" + commonopts "github.com/mongodb/mongo-tools/common/options" + "github.com/mongodb/mongo-tools/common/util" + "github.com/mongodb/mongo-tools/mongotop" + "github.com/mongodb/mongo-tools/mongotop/options" + "github.com/mongodb/mongo-tools/mongotop/output" + "strconv" + "time" +) + +const ( + // the default sleep time, in seconds + DEFAULT_SLEEP_TIME = 1 +) + +func main() { + + // initialize command-line opts + opts := commonopts.New("mongotop", "<options> <sleeptime>") + + // add mongotop-specific options + outputOpts := &options.Output{} + opts.AddOptions(outputOpts) + + extra, err := opts.Parse() + if err != nil { + util.Panicf("error parsing command line options: %v", err) + } + + // print help, if specified + if opts.PrintHelp() { + return + } + + // print version, if specified + if opts.PrintVersion() { + return + } + + // pull out the sleeptime + // TODO: validate args length + sleeptime := DEFAULT_SLEEP_TIME + if len(extra) > 0 { + sleeptime, err = strconv.Atoi(extra[0]) + if err != nil { + util.Panicf("bad sleep time: %v", extra[0]) + } + } + + // create a session provider to connect to the db + sessionProvider, err := db.InitSessionProvider(*opts) + if err != nil { + util.Panicf("error initializing database session: %v", err) + } + + // instantiate a mongotop instance + top := &mongotop.MongoTop{ + Options: opts, + OutputOptions: outputOpts, + Outputter: &output.TerminalOutputter{}, + SessionProvider: sessionProvider, + Sleeptime: time.Duration(sleeptime) * time.Second, + Once: outputOpts.Once, + } + + // kick it off + if err := top.Run(); err != nil { + util.Panicf("error running mongotop: %v", err) + } +} diff --git a/mongotop/mongotop.go b/mongotop/mongotop.go new file mode 100644 index 00000000000..735d484d11a --- /dev/null +++ b/mongotop/mongotop.go @@ -0,0 +1,106 @@ +// Package mongotop implements the core logic and structures +// for the mongotop tool. +package mongotop + +import ( + "fmt" + "github.com/mongodb/mongo-tools/common/db" + commonopts "github.com/mongodb/mongo-tools/common/options" + "github.com/mongodb/mongo-tools/common/util" + "github.com/mongodb/mongo-tools/mongotop/command" + "github.com/mongodb/mongo-tools/mongotop/options" + "github.com/mongodb/mongo-tools/mongotop/output" + "time" +) + +// Wrapper for the mongotop functionality +type MongoTop struct { + // generic mongo tool options + Options *commonopts.ToolOptions + + // mongotop-specific output options + OutputOptions *options.Output + + // for connecting to the db + SessionProvider *db.SessionProvider + + // for outputting the results + output.Outputter + + // the sleep time + Sleeptime time.Duration + + // just run once and finish + Once bool +} + +// Connect to the database and spin, running the top command and outputting +// the results appropriately. +func (self *MongoTop) Run() error { + + // test the connection + session, err := self.SessionProvider.GetSession() + if err != nil { + return err + } + session.Close() + + connUrl := self.Options.Host + if self.Options.Port != "" { + connUrl = connUrl + ":" + self.Options.Port + } + util.Printlnf("connected to: %v", connUrl) + + // the results used to be compared to each other + var previousResults command.Command + if self.OutputOptions.Locks { + previousResults = &command.ServerStatus{} + } else { + previousResults = &command.Top{} + } + + // populate the first run of the previous results + err = self.SessionProvider.RunCommand("admin", previousResults) + if err != nil { + return fmt.Errorf("error running top command: %v", err) + } + + for { + + // sleep + time.Sleep(self.Sleeptime) + + var topResults command.Command + if self.OutputOptions.Locks { + topResults = &command.ServerStatus{} + } else { + topResults = &command.Top{} + } + + // run the top command against the database + err = self.SessionProvider.RunCommand("admin", topResults) + if err != nil { + return fmt.Errorf("error running top command: %v", err) + } + + // diff the results + diff, err := topResults.Diff(previousResults) + if err != nil { + return fmt.Errorf("error computing diff: %v", err) + } + + // output the results + if err := self.Outputter.Output(diff); err != nil { + return fmt.Errorf("error outputting results: %v", err) + } + + // update the previous results + previousResults = topResults + + if self.Once { + return nil + } + + } + +} diff --git a/mongotop/options/options.go b/mongotop/options/options.go new file mode 100644 index 00000000000..c41f7c171aa --- /dev/null +++ b/mongotop/options/options.go @@ -0,0 +1,22 @@ +// Package options implements mongotop-specific command-line options. +package options + +import () + +// Output options for mongotop +type Output struct { + Locks bool `long:"locks" description:"Report on use of per-database locks"` + Once bool `long:"once" description:"Only output stats page once, then quit"` +} + +func (self *Output) Name() string { + return "output" +} + +func (self *Output) PostParse() error { + return nil +} + +func (self *Output) Validate() error { + return nil +} diff --git a/mongotop/output/outputter.go b/mongotop/output/outputter.go new file mode 100644 index 00000000000..7393c116077 --- /dev/null +++ b/mongotop/output/outputter.go @@ -0,0 +1,51 @@ +// Package output implements means of outputting the results of mongotop's +// queries against MongoDB. +package output + +import ( + "fmt" + "github.com/mongodb/mongo-tools/common/util" + "github.com/mongodb/mongo-tools/mongotop/command" + "strings" +) + +// Interface to output the results of the top command. +type Outputter interface { + Output(command.Diff) error +} + +// Outputter that formats the results and prints them to the terminal. +type TerminalOutputter struct { +} + +func (self *TerminalOutputter) Output(diff command.Diff) error { + + tableRows := diff.ToRows() + + // get the length of the longest row (the one with the most fields) + longestRow := 0 + for _, row := range tableRows { + longestRow = util.MaxInt(longestRow, len(row)) + } + + // bookkeep the length of the longest member of each column + longestFields := make([]int, longestRow) + for _, row := range tableRows { + for idx, field := range row { + longestFields[idx] = util.MaxInt(longestFields[idx], len(field)) + } + } + + // write out each row + for _, row := range tableRows { + for idx, rowEl := range row { + fmt.Printf("\t\t%v%v", strings.Repeat(" ", + longestFields[idx]-len(rowEl)), rowEl) + } + fmt.Printf("\n") + } + fmt.Printf("\n") + + return nil + +} diff --git a/mongotop/smoke.sh b/mongotop/smoke.sh new file mode 100755 index 00000000000..5f0d8980f02 --- /dev/null +++ b/mongotop/smoke.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +if ! [ -a mongotop ] +then + echo "need a mongotop binary in the same directory as the smoke script" + exit 1 +fi + +chmod 755 mongotop + +./mongotop > output.out & +mongotop_pid=$! + +sleep 5 + +kill $mongotop_pid + +headers=( "ns" "total" "read" "write" ) +for header in "${headers[@]}" +do + if [ `head -2 output.out | grep -c $header` -ne 1 ] + then + echo "header row doesn't contain $header" + exit 1 + fi +done + +if [ `head -5 output.out | grep -c ms` -ne 3 ] +then + echo "subsequent lines don't contain ms totals" + exit 1 +fi |