summaryrefslogtreecommitdiff
path: root/mongotop
diff options
context:
space:
mode:
authorSam Helman <sam.helman@10gen.com>2014-10-10 13:13:09 -0400
committerSam Helman <sam.helman@10gen.com>2014-10-14 14:08:23 -0400
commit0bc84a9c2194c2ee820a334069e84645a84418e3 (patch)
treed7568597680b251f390aae57bdc45820700c36c9 /mongotop
parent823fb2982e2f3f4a3c26af5681941224bb6882bb (diff)
downloadmongo-0bc84a9c2194c2ee820a334069e84645a84418e3.tar.gz
TOOLS-274: vendor dependencies
Former-commit-id: 3022e68e9a6d1e684364591804671a06c64a27ab
Diffstat (limited to 'mongotop')
-rw-r--r--mongotop/command/command.go20
-rw-r--r--mongotop/command/serverstatus.go94
-rw-r--r--mongotop/command/serverstatus_test.go28
-rw-r--r--mongotop/command/top.go121
-rw-r--r--mongotop/command/top_test.go258
-rw-r--r--mongotop/main/mongotop.go74
-rw-r--r--mongotop/mongotop.go106
-rw-r--r--mongotop/options/options.go22
-rw-r--r--mongotop/output/outputter.go51
-rwxr-xr-xmongotop/smoke.sh33
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