v0.1.3 add support for explicit --defaults-file=xx on command line
[pstop.git] / main.go
1 // pstop - Top like progream which collects information from MySQL's
2 // performance_schema database.
3 package main
4
5 import (
6         "database/sql"
7         "errors"
8         "flag"
9         "fmt"
10         "log"
11         "os"
12         "os/signal"
13         "regexp"
14         "runtime/pprof"
15         "syscall"
16         "time"
17
18         _ "github.com/go-sql-driver/mysql"
19         "github.com/nsf/termbox-go"
20
21         "github.com/sjmudd/mysql_defaults_file"
22         "github.com/sjmudd/pstop/lib"
23         "github.com/sjmudd/pstop/state"
24         "github.com/sjmudd/pstop/version"
25         "github.com/sjmudd/pstop/wait_info"
26 )
27
28 const (
29         sql_driver = "mysql"
30         db         = "performance_schema"
31 )
32
33 var (
34         flag_debug         = flag.Bool("debug", false, "Enabling debug logging")
35         flag_defaults_file = flag.String("defaults-file", "", "Provide a defaults-file to use to connect to MySQL")
36         flag_help          = flag.Bool("help", false, "Provide some help for "+lib.MyName())
37         flag_version       = flag.Bool("version", false, "Show the version of "+lib.MyName())
38         cpuprofile         = flag.String("cpuprofile", "", "write cpu profile to file")
39
40         re_valid_version = regexp.MustCompile(`^(5\.[67]\.|10\.[01])`)
41 )
42
43 // Connect to the database with the given defaults-file, or ~/.my.cnf if not provided.
44 func get_db_handle( defaults_file string ) *sql.DB {
45         var err error
46         var dbh *sql.DB
47         lib.Logger.Println("get_db_handle() connecting to database")
48
49         dbh, err = mysql_defaults_file.OpenUsingDefaultsFile(sql_driver, defaults_file, "performance_schema")
50         if err != nil {
51                 log.Fatal(err)
52         }
53         if err = dbh.Ping(); err != nil {
54                 log.Fatal(err)
55         }
56
57         return dbh
58 }
59
60 // make chan for termbox events and run a poller to send events to the channel
61 // - return the channel
62 func new_tb_chan() chan termbox.Event {
63         termboxChan := make(chan termbox.Event)
64         go func() {
65                 for {
66                         termboxChan <- termbox.PollEvent()
67                 }
68         }()
69         return termboxChan
70 }
71
72 func usage() {
73         fmt.Println(lib.MyName() + " - " + lib.Copyright())
74         fmt.Println("")
75         fmt.Println("Top-like program to show MySQL activity by using information collected")
76         fmt.Println("from performance_schema.")
77         fmt.Println("")
78         fmt.Println("Usage: " + lib.MyName() + " <options>")
79         fmt.Println("")
80         fmt.Println("Options:")
81         fmt.Println("-defaults-file=/path/to/defaults.file   Connect to MySQL using given defaults-file" )
82         fmt.Println("-help                                   show this help message")
83         fmt.Println("-version                                show the version")
84 }
85
86 // pstop requires MySQL 5.6+ or MariaDB 10.0+. Check the version
87 // rather than giving an error message if the requires P_S tables can't
88 // be found.
89 func validate_mysql_version(dbh *sql.DB) error {
90         var tables = [...]string{
91                 "performance_schema.file_summary_by_instance",
92                 "performance_schema.table_io_waits_summary_by_table",
93                 "performance_schema.table_lock_waits_summary_by_table",
94         }
95
96         lib.Logger.Println("validate_mysql_version()")
97
98         lib.Logger.Println("- Getting MySQL version")
99         err, mysql_version := lib.SelectGlobalVariableByVariableName(dbh, "VERSION")
100         if err != nil {
101                 return err
102         }
103         lib.Logger.Println("- mysql_version: '" + mysql_version + "'")
104
105         if !re_valid_version.MatchString(mysql_version) {
106                 err := errors.New(lib.MyName() + " does not work with MySQL version " + mysql_version)
107                 return err
108         }
109         lib.Logger.Println("OK: MySQL version is valid, continuing")
110
111         lib.Logger.Println("Checking access to required tables:")
112         for i := range tables {
113                 if err := lib.CheckTableAccess(dbh, tables[i]); err == nil {
114                         lib.Logger.Println("OK: " + tables[i] + " found")
115                 } else {
116                         return err
117                 }
118         }
119         lib.Logger.Println("OK: all table checks passed")
120
121         return nil
122 }
123
124 func main() {
125         var defaults_file string = ""
126         flag.Parse()
127
128         // clean me up
129         if *cpuprofile != "" {
130                 f, err := os.Create(*cpuprofile)
131                 if err != nil {
132                         log.Fatal(err)
133                 }
134                 pprof.StartCPUProfile(f)
135                 defer pprof.StopCPUProfile()
136         }
137
138         if *flag_debug {
139                 lib.Logger.EnableLogging(true)
140         }
141         if *flag_version {
142                 fmt.Println(lib.MyName() + " version " + version.Version())
143                 return
144         }
145         if *flag_help {
146                 usage()
147                 return
148         }
149
150         lib.Logger.Println("Starting " + lib.MyName())
151
152         if flag_defaults_file != nil && *flag_defaults_file != "" {
153                 defaults_file = *flag_defaults_file
154         }
155
156         dbh := get_db_handle( defaults_file )
157         if err := validate_mysql_version(dbh); err != nil {
158                 log.Fatal(err)
159         }
160
161         var state state.State
162         var wi wait_info.WaitInfo
163         wi.SetWaitInterval(time.Second)
164
165         sigChan := make(chan os.Signal, 1)
166         done := make(chan struct{})
167         defer close(done)
168         termboxChan := new_tb_chan()
169
170         signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
171
172         state.Setup(dbh)
173
174         finished := false
175         for !finished {
176                 select {
177                 case <-done:
178                         fmt.Println("exiting")
179                         finished = true
180                 case sig := <-sigChan:
181                         fmt.Println("Caught a signal", sig)
182                         done <- struct{}{}
183                 case <-wi.WaitNextPeriod():
184                         state.Collect()
185                         wi.CollectedNow()
186                         state.Display()
187                 case event := <-termboxChan:
188                         // switch on event type
189                         switch event.Type {
190                         case termbox.EventKey: // actions depend on key
191                                 switch event.Key {
192                                 case termbox.KeyCtrlZ, termbox.KeyCtrlC, termbox.KeyEsc:
193                                         finished = true
194                                 case termbox.KeyArrowLeft: // left arrow change to previous display mode
195                                         state.DisplayPrevious()
196                                         state.Display()
197                                 case termbox.KeyTab, termbox.KeyArrowRight: // tab or right arrow - change to next display mode
198                                         state.DisplayNext()
199                                         state.Display()
200                                 }
201                                 switch event.Ch {
202                                 case '-': // decrease the interval if > 1
203                                         if wi.WaitInterval() > time.Second {
204                                                 wi.SetWaitInterval(wi.WaitInterval() - time.Second)
205                                         }
206                                 case '+': // increase interval by creating a new ticker
207                                         wi.SetWaitInterval(wi.WaitInterval() + time.Second)
208                                 case 'h', '?': // help
209                                         state.SetHelp(!state.Help())
210                                 case 'q': // quit
211                                         finished = true
212                                 case 't': // toggle between absolute/relative statistics
213                                         state.SetWantRelativeStats(!state.WantRelativeStats())
214                                         state.Display()
215                                 case 'z': // reset the statistics to now by taking a query of current values
216                                         state.ResetDBStatistics()
217                                         state.Display()
218                                 }
219                         case termbox.EventResize: // set sizes
220                                 state.ScreenSetSize(event.Width, event.Height)
221                                 state.Display()
222                         case termbox.EventError: // quit
223                                 log.Fatalf("Quitting because of termbox error: \n%s\n", event.Err)
224                         }
225                 }
226         }
227         state.Cleanup()
228         lib.Logger.Println("Terminating " + lib.MyName())
229 }