From: Simon J Mudd Date: Fri, 6 Feb 2015 17:12:51 +0000 (+0100) Subject: Rename state to app and furhter cleanups X-Git-Url: http://git.iain.cx/?p=pstop.git;a=commitdiff_plain;h=0b7bb57744ef4be1855155bccd96f6370061c009 Rename state to app and furhter cleanups --- diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..1d44bab --- /dev/null +++ b/app/app.go @@ -0,0 +1,624 @@ +// lib - library routines for pstop. +// +// this file contains the library routines related to the stored state in pstop. +package app + +import ( + "database/sql" + "errors" + "fmt" + "log" + "os" + "os/signal" + "regexp" + "strings" + "syscall" + "time" + + "github.com/nsf/termbox-go" + + "github.com/sjmudd/pstop/i_s/processlist" + "github.com/sjmudd/pstop/lib" + essgben "github.com/sjmudd/pstop/p_s/events_stages_summary_global_by_event_name" + ewsgben "github.com/sjmudd/pstop/p_s/events_waits_summary_global_by_event_name" + fsbi "github.com/sjmudd/pstop/p_s/file_summary_by_instance" + "github.com/sjmudd/pstop/p_s/ps_table" + "github.com/sjmudd/pstop/p_s/setup_instruments" + tiwsbt "github.com/sjmudd/pstop/p_s/table_io_waits_summary_by_table" + tlwsbt "github.com/sjmudd/pstop/p_s/table_lock_waits_summary_by_table" + "github.com/sjmudd/pstop/screen" + "github.com/sjmudd/pstop/version" + "github.com/sjmudd/pstop/wait_info" +) + +// what information to show +type Show int + +const ( + showLatency = iota + showOps = iota + showIO = iota + showLocks = iota + showUsers = iota + showMutex = iota + showStages = iota +) + +var ( + re_valid_version = regexp.MustCompile(`^(5\.[67]\.|10\.[01])`) +) + +type App struct { + finished bool + datadir string + dbh *sql.DB + help bool + hostname string + fsbi ps_table.Tabler // ufsbi.File_summary_by_instance + tiwsbt tiwsbt.Object + tlwsbt ps_table.Tabler // tlwsbt.Table_lock_waits_summary_by_table + ewsgben ps_table.Tabler // ewsgben.Events_waits_summary_global_by_event_name + essgben ps_table.Tabler // essgben.Events_stages_summary_global_by_event_name + users processlist.Object + screen screen.TermboxScreen + show Show + mysql_version string + want_relative_stats bool + wait_info.WaitInfo // embedded + setup_instruments setup_instruments.SetupInstruments +} + +func (app *App) Setup(dbh *sql.DB) { + app.dbh = dbh + + if err := app.validate_mysql_version(); err != nil { + log.Fatal(err) + } + + app.finished = false + + app.screen.Initialise() + + app.setup_instruments = setup_instruments.NewSetupInstruments(dbh) + app.setup_instruments.EnableMonitoring() + + _, variables := lib.SelectAllGlobalVariablesByVariableName(app.dbh) + // setup to their initial types/values + app.fsbi = fsbi.NewFileSummaryByInstance(variables) + app.tlwsbt = new(tlwsbt.Object) + app.ewsgben = new(ewsgben.Object) + app.essgben = new(essgben.Object) + + app.want_relative_stats = true // we show info from the point we start collecting data + app.fsbi.SetWantRelativeStats(app.want_relative_stats) + app.fsbi.SetNow() + app.tlwsbt.SetWantRelativeStats(app.want_relative_stats) + app.tlwsbt.SetNow() + app.tiwsbt.SetWantRelativeStats(app.want_relative_stats) + app.tiwsbt.SetNow() + app.users.SetWantRelativeStats(app.want_relative_stats) // ignored + app.users.SetNow() // ignored + app.essgben.SetWantRelativeStats(app.want_relative_stats) + app.essgben.SetNow() + app.ewsgben.SetWantRelativeStats(app.want_relative_stats) // ignored + app.ewsgben.SetNow() // ignored + + app.ResetDBStatistics() + + app.SetHelp(false) + app.show = showLatency + app.tiwsbt.SetWantsLatency(true) + + // get short name (to save space) + _, hostname := lib.SelectGlobalVariableByVariableName(app.dbh, "HOSTNAME") + if index := strings.Index(hostname, "."); index >= 0 { + hostname = hostname[0:index] + } + _, mysql_version := lib.SelectGlobalVariableByVariableName(app.dbh, "VERSION") + _, datadir := lib.SelectGlobalVariableByVariableName(app.dbh, "DATADIR") + app.SetHostname(hostname) + app.SetMySQLVersion(mysql_version) + app.SetDatadir(datadir) +} + +// have we finished ? +func (app App) Finished() bool { + return app.finished +} + +// indicate we have finished +func (app *App) SetFinished() { + app.finished = true +} + +// do a fresh collection of data and then update the initial values based on that. +func (app *App) ResetDBStatistics() { + app.CollectAll() + app.SyncReferenceValues() +} + +func (app *App) SyncReferenceValues() { + start := time.Now() + app.fsbi.SyncReferenceValues() + app.tlwsbt.SyncReferenceValues() + app.tiwsbt.SyncReferenceValues() + app.essgben.SyncReferenceValues() + lib.Logger.Println("app.SyncReferenceValues() took", time.Duration(time.Since(start)).String()) +} + +// collect all initial values on startup / reset +func (app *App) CollectAll() { + app.fsbi.Collect(app.dbh) + app.tlwsbt.Collect(app.dbh) + app.tiwsbt.Collect(app.dbh) +} + +// Only collect the data we are looking at. +func (app *App) Collect() { + start := time.Now() + + switch app.show { + case showLatency, showOps: + app.tiwsbt.Collect(app.dbh) + case showIO: + app.fsbi.Collect(app.dbh) + case showLocks: + app.tlwsbt.Collect(app.dbh) + case showUsers: + app.users.Collect(app.dbh) + case showMutex: + app.ewsgben.Collect(app.dbh) + case showStages: + app.essgben.Collect(app.dbh) + } + lib.Logger.Println("app.Collect() took", time.Duration(time.Since(start)).String()) +} + +func (app App) MySQLVersion() string { + return app.mysql_version +} + +func (app App) Datadir() string { + return app.datadir +} + +func (app *App) SetHelp(newHelp bool) { + app.help = newHelp + + app.screen.Clear() + app.screen.Flush() +} + +func (app *App) SetDatadir(datadir string) { + app.datadir = datadir +} + +func (app *App) SetMySQLVersion(mysql_version string) { + app.mysql_version = mysql_version +} + +func (app *App) SetHostname(hostname string) { + app.hostname = hostname +} + +func (app App) Help() bool { + return app.help +} + +// apps go: showLatency -> showOps -> showIO -> showLocks -> showUsers -> showMutex -> showStages + +// display the output according to the mode we are in +func (app *App) Display() { + if app.help { + app.screen.DisplayHelp() + } else { + app.displayHeading() + switch app.show { + case showLatency, showOps: + app.displayOpsOrLatency() + case showIO: + app.displayIO() + case showLocks: + app.displayLocks() + case showUsers: + app.displayUsers() + case showMutex: + app.displayMutex() + case showStages: + app.displayStages() + } + } +} + +// fix_latency_setting() ensures the SetWantsLatency() value is +// correct. This needs to be done more cleanly. +func (app *App) fix_latency_setting() { + if app.show == showLatency { + app.tiwsbt.SetWantsLatency(true) + } + if app.show == showOps { + app.tiwsbt.SetWantsLatency(false) + } +} + +// change to the previous display mode +func (app *App) DisplayPrevious() { + if app.show == showLatency { + app.show = showStages + } else { + app.show-- + } + app.fix_latency_setting() + app.screen.Clear() + app.screen.Flush() +} + +// change to the next display mode +func (app *App) DisplayNext() { + if app.show == showStages { + app.show = showLatency + } else { + app.show++ + } + app.fix_latency_setting() + app.screen.Clear() + app.screen.Flush() +} + +func (app App) displayHeading() { + app.displayLine0() + app.displayDescription() +} + +func (app App) displayLine0() { + _, uptime := lib.SelectGlobalStatusByVariableName(app.dbh, "UPTIME") + top_line := lib.MyName() + " " + version.Version() + " - " + now_hhmmss() + " " + app.hostname + " / " + app.mysql_version + ", up " + fmt.Sprintf("%-16s", lib.Uptime(uptime)) + if app.want_relative_stats { + now := time.Now() + + var initial time.Time + + switch app.show { + case showLatency, showOps: + initial = app.tiwsbt.Last() + case showIO: + initial = app.fsbi.Last() + case showLocks: + initial = app.tlwsbt.Last() + case showUsers: + initial = app.users.Last() + case showStages: + initial = app.essgben.Last() + case showMutex: + initial = app.ewsgben.Last() + default: + // should not get here ! + } + + d := now.Sub(initial) + + top_line = top_line + " [REL] " + fmt.Sprintf("%.0f seconds", d.Seconds()) + } else { + top_line = top_line + " [ABS] " + } + app.screen.PrintAt(0, 0, top_line) +} + +func (app App) displayDescription() { + description := "UNKNOWN" + + switch app.show { + case showLatency, showOps: + description = app.tiwsbt.Description() + case showIO: + description = app.fsbi.Description() + case showLocks: + description = app.tlwsbt.Description() + case showUsers: + description = app.users.Description() + case showMutex: + description = app.ewsgben.Description() + case showStages: + description = app.essgben.Description() + } + + app.screen.PrintAt(0, 1, description) +} + +func (app *App) displayOpsOrLatency() { + app.screen.BoldPrintAt(0, 2, app.tiwsbt.Headings()) + + max_rows := app.screen.Height() - 3 + row_content := app.tiwsbt.RowContent(max_rows) + + // print out rows + for k := range row_content { + y := 3 + k + app.screen.PrintAt(0, y, row_content[k]) + } + // print out empty rows + for k := len(row_content); k < (app.screen.Height() - 3); k++ { + y := 3 + k + if y < app.screen.Height()-1 { + app.screen.PrintAt(0, y, app.tiwsbt.EmptyRowContent()) + } + } + + // print out the totals at the bottom + app.screen.BoldPrintAt(0, app.screen.Height()-1, app.tiwsbt.TotalRowContent()) +} + +// show actual I/O latency values +func (app App) displayIO() { + app.screen.BoldPrintAt(0, 2, app.fsbi.Headings()) + + // print out the data + max_rows := app.screen.Height() - 3 + row_content := app.fsbi.RowContent(max_rows) + + // print out rows + for k := range row_content { + y := 3 + k + app.screen.PrintAt(0, y, row_content[k]) + } + // print out empty rows + for k := len(row_content); k < (app.screen.Height() - 3); k++ { + y := 3 + k + if y < app.screen.Height()-1 { + app.screen.PrintAt(0, y, app.fsbi.EmptyRowContent()) + } + } + + // print out the totals at the bottom + app.screen.BoldPrintAt(0, app.screen.Height()-1, app.fsbi.TotalRowContent()) +} + +func (app *App) displayLocks() { + app.screen.BoldPrintAt(0, 2, app.tlwsbt.Headings()) + + // print out the data + max_rows := app.screen.Height() - 3 + row_content := app.tlwsbt.RowContent(max_rows) + + // print out rows + for k := range row_content { + y := 3 + k + app.screen.PrintAt(0, y, row_content[k]) + } + // print out empty rows + for k := len(row_content); k < (app.screen.Height() - 3); k++ { + y := 3 + k + if y < app.screen.Height()-1 { + app.screen.PrintAt(0, y, app.tlwsbt.EmptyRowContent()) + } + } + + // print out the totals at the bottom + app.screen.BoldPrintAt(0, app.screen.Height()-1, app.tlwsbt.TotalRowContent()) +} + +func (app *App) displayUsers() { + app.screen.BoldPrintAt(0, 2, app.users.Headings()) + + // print out the data + max_rows := app.screen.Height() - 3 + row_content := app.users.RowContent(max_rows) + + // print out rows + for k := range row_content { + y := 3 + k + app.screen.PrintAt(0, y, row_content[k]) + } + // print out empty rows + for k := len(row_content); k < (app.screen.Height() - 3); k++ { + y := 3 + k + if y < app.screen.Height()-1 { + app.screen.PrintAt(0, y, app.users.EmptyRowContent()) + } + } + + // print out the totals at the bottom + app.screen.BoldPrintAt(0, app.screen.Height()-1, app.users.TotalRowContent()) +} + +func (app *App) displayMutex() { + app.screen.BoldPrintAt(0, 2, app.ewsgben.Headings()) + + // print out the data + max_rows := app.screen.Height() - 3 + row_content := app.ewsgben.RowContent(max_rows) + + // print out rows + for k := range row_content { + y := 3 + k + app.screen.PrintAt(0, y, row_content[k]) + } + // print out empty rows + for k := len(row_content); k < (app.screen.Height() - 3); k++ { + y := 3 + k + if y < app.screen.Height()-1 { + app.screen.PrintAt(0, y, app.ewsgben.EmptyRowContent()) + } + } + + // print out the totals at the bottom + app.screen.BoldPrintAt(0, app.screen.Height()-1, app.ewsgben.TotalRowContent()) +} + +func (app *App) displayStages() { + app.screen.BoldPrintAt(0, 2, app.essgben.Headings()) + + // print out the data + max_rows := app.screen.Height() - 3 + row_content := app.essgben.RowContent(max_rows) + + // print out rows + for k := range row_content { + y := 3 + k + app.screen.PrintAt(0, y, row_content[k]) + } + // print out empty rows + for k := len(row_content); k < (app.screen.Height() - 3); k++ { + y := 3 + k + if y < app.screen.Height()-1 { + app.screen.PrintAt(0, y, app.essgben.EmptyRowContent()) + } + } + + // print out the totals at the bottom + app.screen.BoldPrintAt(0, app.screen.Height()-1, app.essgben.TotalRowContent()) +} + +// do we want to show all p_s data? +func (app App) WantRelativeStats() bool { + return app.want_relative_stats +} + +// set if we want data from when we started/reset stats. +func (app *App) SetWantRelativeStats(want_relative_stats bool) { + app.want_relative_stats = want_relative_stats + + app.fsbi.SetWantRelativeStats(want_relative_stats) + app.tlwsbt.SetWantRelativeStats(app.want_relative_stats) + app.tiwsbt.SetWantRelativeStats(app.want_relative_stats) + app.ewsgben.SetWantRelativeStats(app.want_relative_stats) + app.essgben.SetWantRelativeStats(app.want_relative_stats) +} + +// if there's a better way of doing this do it better ... +func now_hhmmss() string { + t := time.Now() + return fmt.Sprintf("%2d:%02d:%02d", t.Hour(), t.Minute(), t.Second()) +} + +// record the latest screen size +func (app *App) ScreenSetSize(width, height int) { + app.screen.SetSize(width, height) +} + +// clean up screen and disconnect database +func (app *App) Cleanup() { + app.screen.Close() + if app.dbh != nil { + app.setup_instruments.RestoreConfiguration() + _ = app.dbh.Close() + } +} + +// make chan for termbox events and run a poller to send events to the channel +// - return the channel +func new_tb_chan() chan termbox.Event { + termboxChan := make(chan termbox.Event) + go func() { + for { + termboxChan <- termbox.PollEvent() + } + }() + return termboxChan +} + +// get into a run loop +func (app *App) Run() { + done := make(chan struct{}) + defer close(done) + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + var wi wait_info.WaitInfo + wi.SetWaitInterval(time.Second) + + termboxChan := new_tb_chan() + + for !app.Finished() { + select { + case <-done: + fmt.Println("exiting") + app.SetFinished() + case sig := <-sigChan: + fmt.Println("Caught a signal", sig) + done <- struct{}{} + case <-wi.WaitNextPeriod(): + app.Collect() + wi.CollectedNow() + app.Display() + case event := <-termboxChan: + // switch on event type + switch event.Type { + case termbox.EventKey: // actions depend on key + switch event.Key { + case termbox.KeyCtrlZ, termbox.KeyCtrlC, termbox.KeyEsc: + app.SetFinished() + case termbox.KeyArrowLeft: // left arrow change to previous display mode + app.DisplayPrevious() + app.Display() + case termbox.KeyTab, termbox.KeyArrowRight: // tab or right arrow - change to next display mode + app.DisplayNext() + app.Display() + } + switch event.Ch { + case '-': // decrease the interval if > 1 + if wi.WaitInterval() > time.Second { + wi.SetWaitInterval(wi.WaitInterval() - time.Second) + } + case '+': // increase interval by creating a new ticker + wi.SetWaitInterval(wi.WaitInterval() + time.Second) + case 'h', '?': // help + app.SetHelp(!app.Help()) + case 'q': // quit + app.SetFinished() + case 't': // toggle between absolute/relative statistics + app.SetWantRelativeStats(!app.WantRelativeStats()) + app.Display() + case 'z': // reset the statistics to now by taking a query of current values + app.ResetDBStatistics() + app.Display() + } + case termbox.EventResize: // set sizes + app.ScreenSetSize(event.Width, event.Height) + app.Display() + case termbox.EventError: // quit + log.Fatalf("Quitting because of termbox error: \n%s\n", event.Err) + } + } + } +} + +// pstop requires MySQL 5.6+ or MariaDB 10.0+. Check the version +// rather than giving an error message if the requires P_S tables can't +// be found. +func (app *App) validate_mysql_version() error { + var tables = [...]string{ + "performance_schema.events_waits_summary_global_by_event_name", + "performance_schema.file_summary_by_instance", + "performance_schema.table_io_waits_summary_by_table", + "performance_schema.table_lock_waits_summary_by_table", + } + + lib.Logger.Println("validate_mysql_version()") + + lib.Logger.Println("- Getting MySQL version") + err, mysql_version := lib.SelectGlobalVariableByVariableName(app.dbh, "VERSION") + if err != nil { + return err + } + lib.Logger.Println("- mysql_version: '" + mysql_version + "'") + + if !re_valid_version.MatchString(mysql_version) { + return errors.New(lib.MyName() + " does not work with MySQL version " + mysql_version) + } + lib.Logger.Println("OK: MySQL version is valid, continuing") + + lib.Logger.Println("Checking access to required tables:") + for i := range tables { + if err := lib.CheckTableAccess(app.dbh, tables[i]); err == nil { + lib.Logger.Println("OK: " + tables[i] + " found") + } else { + return err + } + } + lib.Logger.Println("OK: all table checks passed") + + return nil +} + diff --git a/connector/connector.go b/connector/connector.go new file mode 100644 index 0000000..53077e8 --- /dev/null +++ b/connector/connector.go @@ -0,0 +1,104 @@ +// Use Connector to specify how to connect to MySQL. +// Then get a sql.*DB from it which is returned to the app.. +package connector + +import ( + "database/sql" + "log" + + _ "github.com/go-sql-driver/mysql" + + "github.com/sjmudd/mysql_defaults_file" + "github.com/sjmudd/pstop/lib" +) + +const ( + sql_driver = "mysql" + db = "performance_schema" + + DEFAULTS_FILE = iota + COMPONENTS = iota +) + +// connector struct +type Connector struct { + connectBy int + components map[string] string + defaults_file string + dbh *sql.DB +} + +// return the database handle +func (c Connector) Handle() *sql.DB { + return c.dbh +} + +// return the defaults file +func (c Connector) DefaultsFile() string { + return c.defaults_file +} + +// set the defaults file +func (c *Connector) SetDefaultsFile( defaults_file string ) { + c.defaults_file = defaults_file +} + +// set the components +func (c *Connector) SetComponents(components map[string]string) { + c.components = components +} + +// things to do after connecting +func (c *Connector) postConnectStuff() { + if err := c.dbh.Ping(); err != nil { + log.Fatal(err) + } + + // deliberately limit the pool size to 5 to avoid "problems" if any queries hang. + c.dbh.SetMaxOpenConns(5) // hard-coded value! +} + +// determine how we want to connect +func (c *Connector) SetConnectBy( connectHow int ) { + c.connectBy = connectHow +} + +// make the database connection +func (c *Connector) Connect() { + var err error + + switch { + case c.connectBy == DEFAULTS_FILE: + lib.Logger.Println("connect_by_defaults_file() connecting to database") + + c.dbh, err = mysql_defaults_file.OpenUsingDefaultsFile(sql_driver, c.defaults_file, db) + if err != nil { + log.Fatal(err) + } + case c.connectBy == COMPONENTS: + lib.Logger.Println("connect_by_components() connecting to database") + + new_dsn := mysql_defaults_file.BuildDSN(c.components, db) + c.dbh, err = sql.Open(sql_driver, new_dsn) + if err != nil { + log.Fatal(err) + } + default: + log.Fatal("Connector.Connect() c.connectBy not DEFAULTS_FILE/COMPONENTS") + } + c.postConnectStuff() +} + +// Connect to MySQL using various component parts needed to make the dsn. +func (c *Connector) ConnectByComponents(components map[string]string) { + c.SetComponents(components) + c.SetConnectBy(COMPONENTS) + c.Connect() +} + +// Connect to the database with the given defaults-file, or ~/.my.cnf if not provided. +func (c *Connector) ConnectByDefaultsFile(defaults_file string) { + c.SetDefaultsFile(defaults_file) + c.SetConnectBy(DEFAULTS_FILE) + c.Connect() +} diff --git a/i_s/processlist/public.go b/i_s/processlist/public.go index dcd0a25..4a75c1d 100644 --- a/i_s/processlist/public.go +++ b/i_s/processlist/public.go @@ -20,9 +20,9 @@ type map_string_int map[string]int type Object struct { p_s.RelativeStats p_s.InitialTime - current table_rows // processlist - results pl_by_user_rows // results by user - totals pl_by_user_row // totals of results + current table_rows // processlist + results pl_by_user_rows // results by user + totals pl_by_user_row // totals of results } // Collect() collects data from the db, updating initial diff --git a/key_value_cache/key_value_cache.go b/key_value_cache/key_value_cache.go index 9b689a2..9292de5 100644 --- a/key_value_cache/key_value_cache.go +++ b/key_value_cache/key_value_cache.go @@ -20,7 +20,7 @@ type KeyValueCache struct { func NewKeyValueCache() KeyValueCache { lib.Logger.Println("KeyValueCache()") - return KeyValueCache {} + return KeyValueCache{} } // Given a lookup key return the value if found. diff --git a/lib/common.go b/lib/common.go index 3cee791..d93f776 100644 --- a/lib/common.go +++ b/lib/common.go @@ -138,13 +138,13 @@ func FormatAmount(amount uint64) string { } // like Amount but tigher in space -func FormatCounter( counter int, width int ) string { +func FormatCounter(counter int, width int) string { if counter == 0 { pattern := "%" + fmt.Sprintf("%d", width) + "s" - return fmt.Sprintf( pattern, " " ) + return fmt.Sprintf(pattern, " ") } else { pattern := "%" + fmt.Sprintf("%d", width) + "d" - return fmt.Sprintf( pattern, counter ) + return fmt.Sprintf(pattern, counter) } } diff --git a/main.go b/main.go index facfa65..2d218f9 100644 --- a/main.go +++ b/main.go @@ -3,26 +3,18 @@ package main import ( - "database/sql" - "errors" "flag" "fmt" "log" "os" - "os/signal" - "regexp" "runtime/pprof" - "syscall" - "time" _ "github.com/go-sql-driver/mysql" - "github.com/nsf/termbox-go" - "github.com/sjmudd/mysql_defaults_file" + "github.com/sjmudd/pstop/app" + "github.com/sjmudd/pstop/connector" "github.com/sjmudd/pstop/lib" - "github.com/sjmudd/pstop/state" "github.com/sjmudd/pstop/version" - "github.com/sjmudd/pstop/wait_info" ) const ( @@ -35,66 +27,14 @@ var ( flag_defaults_file = flag.String("defaults-file", "", "Provide a defaults-file to use to connect to MySQL") flag_help = flag.Bool("help", false, "Provide some help for "+lib.MyName()) flag_host = flag.String("host", "", "Provide the hostname of the MySQL to connect to") - flag_port = flag.Int("port", 0 , "Provide the port number of the MySQL to connect to (default: 3306)") /* deliberately 0 here, defaults to 3306 elsewhere */ + flag_port = flag.Int("port", 0, "Provide the port number of the MySQL to connect to (default: 3306)") /* deliberately 0 here, defaults to 3306 elsewhere */ flag_socket = flag.String("socket", "", "Provide the path to the local MySQL server to connect to") flag_password = flag.String("password", "", "Provide the password when connecting to the MySQL server") flag_user = flag.String("user", "", "Provide the username to connect with to MySQL (default: $USER)") flag_version = flag.Bool("version", false, "Show the version of "+lib.MyName()) cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") - - re_valid_version = regexp.MustCompile(`^(5\.[67]\.|10\.[01])`) ) -// Connect to the database with the given defaults-file, or ~/.my.cnf if not provided. -func connect_by_defaults_file( defaults_file string ) *sql.DB { - var err error - var dbh *sql.DB - lib.Logger.Println("connect_by_defaults_file() connecting to database") - - dbh, err = mysql_defaults_file.OpenUsingDefaultsFile(sql_driver, defaults_file, "performance_schema") - if err != nil { - log.Fatal(err) - } - if err = dbh.Ping(); err != nil { - log.Fatal(err) - } - - // deliberately limit the pool size to 5 to avoid "problems" if any queries hang. - dbh.SetMaxOpenConns(5) // hard-coded value! - - return dbh -} - -// connect to MySQL using various component parts needed to make the dsn -func connect_by_components( components map[string]string ) *sql.DB { - var err error - var dbh *sql.DB - lib.Logger.Println("connect_by_components() connecting to database") - - new_dsn := mysql_defaults_file.BuildDSN(components, "performance_schema") - dbh, err = sql.Open(sql_driver, new_dsn) - if err != nil { - log.Fatal(err) - } - if err = dbh.Ping(); err != nil { - log.Fatal(err) - } - - return dbh -} - -// make chan for termbox events and run a poller to send events to the channel -// - return the channel -func new_tb_chan() chan termbox.Event { - termboxChan := make(chan termbox.Event) - go func() { - for { - termboxChan <- termbox.PollEvent() - } - }() - return termboxChan -} - func usage() { fmt.Println(lib.MyName() + " - " + lib.Copyright()) fmt.Println("") @@ -104,7 +44,7 @@ func usage() { fmt.Println("Usage: " + lib.MyName() + " ") fmt.Println("") fmt.Println("Options:") - fmt.Println("--defaults-file=/path/to/defaults.file Connect to MySQL using given defaults-file" ) + fmt.Println("--defaults-file=/path/to/defaults.file Connect to MySQL using given defaults-file") fmt.Println("--help Show this help message") fmt.Println("--version Show the version") fmt.Println("--host= MySQL host to connect to") @@ -114,45 +54,6 @@ func usage() { fmt.Println("--password= Password to use when connecting") } -// pstop requires MySQL 5.6+ or MariaDB 10.0+. Check the version -// rather than giving an error message if the requires P_S tables can't -// be found. -func validate_mysql_version(dbh *sql.DB) error { - var tables = [...]string{ - "performance_schema.events_waits_summary_global_by_event_name", - "performance_schema.file_summary_by_instance", - "performance_schema.table_io_waits_summary_by_table", - "performance_schema.table_lock_waits_summary_by_table", - } - - lib.Logger.Println("validate_mysql_version()") - - lib.Logger.Println("- Getting MySQL version") - err, mysql_version := lib.SelectGlobalVariableByVariableName(dbh, "VERSION") - if err != nil { - return err - } - lib.Logger.Println("- mysql_version: '" + mysql_version + "'") - - if !re_valid_version.MatchString(mysql_version) { - err := errors.New(lib.MyName() + " does not work with MySQL version " + mysql_version) - return err - } - lib.Logger.Println("OK: MySQL version is valid, continuing") - - lib.Logger.Println("Checking access to required tables:") - for i := range tables { - if err := lib.CheckTableAccess(dbh, tables[i]); err == nil { - lib.Logger.Println("OK: " + tables[i] + " found") - } else { - return err - } - } - lib.Logger.Println("OK: all table checks passed") - - return nil -} - func main() { var defaults_file string = "" flag.Parse() @@ -181,13 +82,13 @@ func main() { lib.Logger.Println("Starting " + lib.MyName()) - var dbh *sql.DB + var connector connector.Connector if *flag_host != "" || *flag_socket != "" { lib.Logger.Println("--host= or --socket= defined") var components = make(map[string]string) if *flag_host != "" && *flag_socket != "" { - fmt.Println(lib.MyName() + ": Do not specify --host and --socket together" ) + fmt.Println(lib.MyName() + ": Do not specify --host and --socket together") os.Exit(1) } if *flag_host != "" { @@ -197,7 +98,7 @@ func main() { if *flag_socket == "" { components["port"] = fmt.Sprintf("%d", *flag_port) } else { - fmt.Println(lib.MyName() + ": Do not specify --socket and --port together" ) + fmt.Println(lib.MyName() + ": Do not specify --socket and --port together") os.Exit(1) } } @@ -210,85 +111,21 @@ func main() { if *flag_password != "" { components["password"] = *flag_password } - dbh = connect_by_components( components ) + connector.ConnectByComponents(components) } else { - if flag_defaults_file != nil && *flag_defaults_file != "" { + if flag_defaults_file != nil && *flag_defaults_file != "" { lib.Logger.Println("--defaults-file defined") defaults_file = *flag_defaults_file } else { lib.Logger.Println("connecting by implicit defaults file") } - dbh = connect_by_defaults_file( defaults_file ) + connector.ConnectByDefaultsFile(defaults_file) } - if err := validate_mysql_version(dbh); err != nil { - log.Fatal(err) - } - - var state state.State - var wi wait_info.WaitInfo - wi.SetWaitInterval(time.Second) - - sigChan := make(chan os.Signal, 1) - done := make(chan struct{}) - defer close(done) - termboxChan := new_tb_chan() + var app app.App - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - state.Setup(dbh) - for !state.Finished() { - select { - case <-done: - fmt.Println("exiting") - state.SetFinished() - case sig := <-sigChan: - fmt.Println("Caught a signal", sig) - done <- struct{}{} - case <-wi.WaitNextPeriod(): - state.Collect() - wi.CollectedNow() - state.Display() - case event := <-termboxChan: - // switch on event type - switch event.Type { - case termbox.EventKey: // actions depend on key - switch event.Key { - case termbox.KeyCtrlZ, termbox.KeyCtrlC, termbox.KeyEsc: - state.SetFinished() - case termbox.KeyArrowLeft: // left arrow change to previous display mode - state.DisplayPrevious() - state.Display() - case termbox.KeyTab, termbox.KeyArrowRight: // tab or right arrow - change to next display mode - state.DisplayNext() - state.Display() - } - switch event.Ch { - case '-': // decrease the interval if > 1 - if wi.WaitInterval() > time.Second { - wi.SetWaitInterval(wi.WaitInterval() - time.Second) - } - case '+': // increase interval by creating a new ticker - wi.SetWaitInterval(wi.WaitInterval() + time.Second) - case 'h', '?': // help - state.SetHelp(!state.Help()) - case 'q': // quit - state.SetFinished() - case 't': // toggle between absolute/relative statistics - state.SetWantRelativeStats(!state.WantRelativeStats()) - state.Display() - case 'z': // reset the statistics to now by taking a query of current values - state.ResetDBStatistics() - state.Display() - } - case termbox.EventResize: // set sizes - state.ScreenSetSize(event.Width, event.Height) - state.Display() - case termbox.EventError: // quit - log.Fatalf("Quitting because of termbox error: \n%s\n", event.Err) - } - } - } - state.Cleanup() + app.Setup(connector.Handle()) + app.Run() + app.Cleanup() lib.Logger.Println("Terminating " + lib.MyName()) } diff --git a/p_s/setup_instruments/setup_instruments.go b/p_s/setup_instruments/setup_instruments.go index 8a93493..3475be4 100644 --- a/p_s/setup_instruments/setup_instruments.go +++ b/p_s/setup_instruments/setup_instruments.go @@ -37,7 +37,7 @@ type SetupInstruments struct { // Return a newly initialised SetupInstruments structure with a handle to the database. // Better to return a pointer ? func NewSetupInstruments(dbh *sql.DB) SetupInstruments { - return SetupInstruments{ dbh: dbh } + return SetupInstruments{dbh: dbh} } // enable mutex and stage monitoring @@ -144,7 +144,7 @@ func (si *SetupInstruments) Configure(select_sql string, collecting, updating st count = 0 for i := range si.rows { lib.Logger.Println("- changing row:", si.rows[i].NAME) - lib.Logger.Println("stmt.Exec", "YES", "YES", si.rows[i].NAME ) + lib.Logger.Println("stmt.Exec", "YES", "YES", si.rows[i].NAME) if res, err := stmt.Exec("YES", "YES", si.rows[i].NAME); err == nil { lib.Logger.Println("update succeeded") si.update_succeeded = true @@ -166,7 +166,7 @@ func (si *SetupInstruments) Configure(select_sql string, collecting, updating st } stmt.Close() } - lib.Logger.Println( "Configure() returns update_tried", si.update_tried, ", update_succeeded", si.update_succeeded) + lib.Logger.Println("Configure() returns update_tried", si.update_tried, ", update_succeeded", si.update_succeeded) } // restore setup_instruments rows to their previous settings @@ -182,14 +182,14 @@ func (si *SetupInstruments) RestoreConfiguration() { // update the rows which need to be set - do multiple updates but I don't care update_sql := "UPDATE setup_instruments SET enabled = ?, TIMED = ? WHERE NAME = ?" - lib.Logger.Println("dbh.Prepare(",update_sql,")") + lib.Logger.Println("dbh.Prepare(", update_sql, ")") stmt, err := si.dbh.Prepare(update_sql) if err != nil { log.Fatal(err) } count := 0 for i := range si.rows { - lib.Logger.Println("stmt.Exec(",si.rows[i].ENABLED, si.rows[i].TIMED, si.rows[i].NAME,")") + lib.Logger.Println("stmt.Exec(", si.rows[i].ENABLED, si.rows[i].TIMED, si.rows[i].NAME, ")") if _, err := stmt.Exec(si.rows[i].ENABLED, si.rows[i].TIMED, si.rows[i].NAME); err != nil { log.Fatal(err) } diff --git a/screen/screen.go b/screen/screen.go index f428da8..4484e45 100644 --- a/screen/screen.go +++ b/screen/screen.go @@ -83,7 +83,7 @@ func (s *TermboxScreen) BoldPrintAt(x int, y int, text string) { offset := 0 for c := range text { if (x + offset) < s.width { - termbox.SetCell(x+offset, y, rune(text[c]), s.fg | termbox.AttrBold, s.bg) + termbox.SetCell(x+offset, y, rune(text[c]), s.fg|termbox.AttrBold, s.bg) offset++ } } diff --git a/state/state.go b/state/state.go deleted file mode 100644 index de0d4ff..0000000 --- a/state/state.go +++ /dev/null @@ -1,490 +0,0 @@ -// lib - library routines for pstop. -// -// this file contains the library routines related to the stored state in pstop. -package state - -import ( - "database/sql" - "fmt" - "strings" - "time" - - "github.com/sjmudd/pstop/i_s/processlist" - "github.com/sjmudd/pstop/lib" - ewsgben "github.com/sjmudd/pstop/p_s/events_waits_summary_global_by_event_name" - essgben "github.com/sjmudd/pstop/p_s/events_stages_summary_global_by_event_name" - fsbi "github.com/sjmudd/pstop/p_s/file_summary_by_instance" - "github.com/sjmudd/pstop/p_s/ps_table" - "github.com/sjmudd/pstop/p_s/setup_instruments" - tiwsbt "github.com/sjmudd/pstop/p_s/table_io_waits_summary_by_table" - tlwsbt "github.com/sjmudd/pstop/p_s/table_lock_waits_summary_by_table" - "github.com/sjmudd/pstop/screen" - "github.com/sjmudd/pstop/version" - "github.com/sjmudd/pstop/wait_info" -) - -// what information to show -type Show int - -const ( - showLatency = iota - showOps = iota - showIO = iota - showLocks = iota - showUsers = iota - showMutex = iota - showStages = iota -) - -type State struct { - finished bool - datadir string - dbh *sql.DB - help bool - hostname string - fsbi ps_table.Tabler // ufsbi.File_summary_by_instance - tiwsbt tiwsbt.Object - tlwsbt ps_table.Tabler // tlwsbt.Table_lock_waits_summary_by_table - ewsgben ps_table.Tabler // ewsgben.Events_waits_summary_global_by_event_name - essgben ps_table.Tabler // essgben.Events_stages_summary_global_by_event_name - users processlist.Object - screen screen.TermboxScreen - show Show - mysql_version string - want_relative_stats bool - wait_info.WaitInfo // embedded - setup_instruments setup_instruments.SetupInstruments -} - -func (state *State) Setup(dbh *sql.DB) { - state.dbh = dbh - state.finished = false - - state.screen.Initialise() - - state.setup_instruments = setup_instruments.NewSetupInstruments(dbh) - state.setup_instruments.EnableMonitoring() - - _, variables := lib.SelectAllGlobalVariablesByVariableName(state.dbh) - // setup to their initial types/values - state.fsbi = fsbi.NewFileSummaryByInstance(variables) - state.tlwsbt = new(tlwsbt.Object) - state.ewsgben = new(ewsgben.Object) - state.essgben = new(essgben.Object) - - state.want_relative_stats = true // we show info from the point we start collecting data - state.fsbi.SetWantRelativeStats(state.want_relative_stats) - state.fsbi.SetNow() - state.tlwsbt.SetWantRelativeStats(state.want_relative_stats) - state.tlwsbt.SetNow() - state.tiwsbt.SetWantRelativeStats(state.want_relative_stats) - state.tiwsbt.SetNow() - state.users.SetWantRelativeStats(state.want_relative_stats) // ignored - state.users.SetNow() // ignored - state.essgben.SetWantRelativeStats(state.want_relative_stats) - state.essgben.SetNow() - state.ewsgben.SetWantRelativeStats(state.want_relative_stats) // ignored - state.ewsgben.SetNow() // ignored - - state.ResetDBStatistics() - - state.SetHelp(false) - state.show = showLatency - state.tiwsbt.SetWantsLatency(true) - - // get short name (to save space) - _, hostname := lib.SelectGlobalVariableByVariableName(state.dbh, "HOSTNAME") - if index := strings.Index(hostname, "."); index >= 0 { - hostname = hostname[0:index] - } - _, mysql_version := lib.SelectGlobalVariableByVariableName(state.dbh, "VERSION") - _, datadir := lib.SelectGlobalVariableByVariableName(state.dbh, "DATADIR") - state.SetHostname(hostname) - state.SetMySQLVersion(mysql_version) - state.SetDatadir(datadir) -} - -// have we finished ? -func (state State) Finished() bool { - return state.finished -} - -// indicate we have finished -func (state *State) SetFinished() { - state.finished = true -} - -// do a fresh collection of data and then update the initial values based on that. -func (state *State) ResetDBStatistics() { - state.CollectAll() - state.SyncReferenceValues() -} - -func (state *State) SyncReferenceValues() { - start := time.Now() - state.fsbi.SyncReferenceValues() - state.tlwsbt.SyncReferenceValues() - state.tiwsbt.SyncReferenceValues() - state.essgben.SyncReferenceValues() - lib.Logger.Println("state.SyncReferenceValues() took", time.Duration(time.Since(start)).String()) -} - -// collect all initial values on startup / reset -func (state *State) CollectAll() { - state.fsbi.Collect(state.dbh) - state.tlwsbt.Collect(state.dbh) - state.tiwsbt.Collect(state.dbh) -} - -// Only collect the data we are looking at. -func (state *State) Collect() { - start := time.Now() - - switch state.show { - case showLatency, showOps: - state.tiwsbt.Collect(state.dbh) - case showIO: - state.fsbi.Collect(state.dbh) - case showLocks: - state.tlwsbt.Collect(state.dbh) - case showUsers: - state.users.Collect(state.dbh) - case showMutex: - state.ewsgben.Collect(state.dbh) - case showStages: - state.essgben.Collect(state.dbh) - } - lib.Logger.Println("state.Collect() took", time.Duration(time.Since(start)).String()) -} - -func (state State) MySQLVersion() string { - return state.mysql_version -} - -func (state State) Datadir() string { - return state.datadir -} - -func (state *State) SetHelp(newHelp bool) { - state.help = newHelp - - state.screen.Clear() - state.screen.Flush() -} - -func (state *State) SetDatadir(datadir string) { - state.datadir = datadir -} - -func (state *State) SetMySQLVersion(mysql_version string) { - state.mysql_version = mysql_version -} - -func (state *State) SetHostname(hostname string) { - state.hostname = hostname -} - -func (state State) Help() bool { - return state.help -} - -// states go: showLatency -> showOps -> showIO -> showLocks -> showUsers -> showMutex -> showStages - -// display the output according to the mode we are in -func (state *State) Display() { - if state.help { - state.screen.DisplayHelp() - } else { - state.displayHeading() - switch state.show { - case showLatency, showOps: - state.displayOpsOrLatency() - case showIO: - state.displayIO() - case showLocks: - state.displayLocks() - case showUsers: - state.displayUsers() - case showMutex: - state.displayMutex() - case showStages: - state.displayStages() - } - } -} - -// fix_latency_setting() ensures the SetWantsLatency() value is -// correct. This needs to be done more cleanly. -func (state *State) fix_latency_setting() { - if state.show == showLatency { - state.tiwsbt.SetWantsLatency(true) - } - if state.show == showOps { - state.tiwsbt.SetWantsLatency(false) - } -} - -// change to the previous display mode -func (state *State) DisplayPrevious() { - if state.show == showLatency { - state.show = showStages - } else { - state.show-- - } - state.fix_latency_setting() - state.screen.Clear() - state.screen.Flush() -} - -// change to the next display mode -func (state *State) DisplayNext() { - if state.show == showStages { - state.show = showLatency - } else { - state.show++ - } - state.fix_latency_setting() - state.screen.Clear() - state.screen.Flush() -} - -func (state State) displayHeading() { - state.displayLine0() - state.displayDescription() -} - -func (state State) displayLine0() { - _, uptime := lib.SelectGlobalStatusByVariableName(state.dbh, "UPTIME") - top_line := lib.MyName() + " " + version.Version() + " - " + now_hhmmss() + " " + state.hostname + " / " + state.mysql_version + ", up " + fmt.Sprintf("%-16s", lib.Uptime(uptime)) - if state.want_relative_stats { - now := time.Now() - - var initial time.Time - - switch state.show { - case showLatency, showOps: - initial = state.tiwsbt.Last() - case showIO: - initial = state.fsbi.Last() - case showLocks: - initial = state.tlwsbt.Last() - case showUsers: - initial = state.users.Last() - case showStages: - initial = state.essgben.Last() - case showMutex: - initial = state.ewsgben.Last() - default: - // should not get here ! - } - - d := now.Sub(initial) - - top_line = top_line + " [REL] " + fmt.Sprintf("%.0f seconds", d.Seconds()) - } else { - top_line = top_line + " [ABS] " - } - state.screen.PrintAt(0, 0, top_line) -} - -func (state State) displayDescription() { - description := "UNKNOWN" - - switch state.show { - case showLatency, showOps: - description = state.tiwsbt.Description() - case showIO: - description = state.fsbi.Description() - case showLocks: - description = state.tlwsbt.Description() - case showUsers: - description = state.users.Description() - case showMutex: - description = state.ewsgben.Description() - case showStages: - description = state.essgben.Description() - } - - state.screen.PrintAt(0, 1, description) -} - -func (state *State) displayOpsOrLatency() { - state.screen.BoldPrintAt(0, 2, state.tiwsbt.Headings()) - - max_rows := state.screen.Height() - 3 - row_content := state.tiwsbt.RowContent(max_rows) - - // print out rows - for k := range row_content { - y := 3 + k - state.screen.PrintAt(0, y, row_content[k]) - } - // print out empty rows - for k := len(row_content); k < (state.screen.Height() - 3); k++ { - y := 3 + k - if y < state.screen.Height()-1 { - state.screen.PrintAt(0, y, state.tiwsbt.EmptyRowContent()) - } - } - - // print out the totals at the bottom - state.screen.BoldPrintAt(0, state.screen.Height()-1, state.tiwsbt.TotalRowContent()) -} - -// show actual I/O latency values -func (state State) displayIO() { - state.screen.BoldPrintAt(0, 2, state.fsbi.Headings()) - - // print out the data - max_rows := state.screen.Height() - 3 - row_content := state.fsbi.RowContent(max_rows) - - // print out rows - for k := range row_content { - y := 3 + k - state.screen.PrintAt(0, y, row_content[k]) - } - // print out empty rows - for k := len(row_content); k < (state.screen.Height() - 3); k++ { - y := 3 + k - if y < state.screen.Height()-1 { - state.screen.PrintAt(0, y, state.fsbi.EmptyRowContent()) - } - } - - // print out the totals at the bottom - state.screen.BoldPrintAt(0, state.screen.Height()-1, state.fsbi.TotalRowContent()) -} - -func (state *State) displayLocks() { - state.screen.BoldPrintAt(0, 2, state.tlwsbt.Headings()) - - // print out the data - max_rows := state.screen.Height() - 3 - row_content := state.tlwsbt.RowContent(max_rows) - - // print out rows - for k := range row_content { - y := 3 + k - state.screen.PrintAt(0, y, row_content[k]) - } - // print out empty rows - for k := len(row_content); k < (state.screen.Height() - 3); k++ { - y := 3 + k - if y < state.screen.Height()-1 { - state.screen.PrintAt(0, y, state.tlwsbt.EmptyRowContent()) - } - } - - // print out the totals at the bottom - state.screen.BoldPrintAt(0, state.screen.Height()-1, state.tlwsbt.TotalRowContent()) -} - -func (state *State) displayUsers() { - state.screen.BoldPrintAt(0, 2, state.users.Headings()) - - // print out the data - max_rows := state.screen.Height() - 3 - row_content := state.users.RowContent(max_rows) - - // print out rows - for k := range row_content { - y := 3 + k - state.screen.PrintAt(0, y, row_content[k]) - } - // print out empty rows - for k := len(row_content); k < (state.screen.Height() - 3); k++ { - y := 3 + k - if y < state.screen.Height()-1 { - state.screen.PrintAt(0, y, state.users.EmptyRowContent()) - } - } - - // print out the totals at the bottom - state.screen.BoldPrintAt(0, state.screen.Height()-1, state.users.TotalRowContent()) -} - -func (state *State) displayMutex() { - state.screen.BoldPrintAt(0, 2, state.ewsgben.Headings()) - - // print out the data - max_rows := state.screen.Height() - 3 - row_content := state.ewsgben.RowContent(max_rows) - - // print out rows - for k := range row_content { - y := 3 + k - state.screen.PrintAt(0, y, row_content[k]) - } - // print out empty rows - for k := len(row_content); k < (state.screen.Height() - 3); k++ { - y := 3 + k - if y < state.screen.Height()-1 { - state.screen.PrintAt(0, y, state.ewsgben.EmptyRowContent()) - } - } - - // print out the totals at the bottom - state.screen.BoldPrintAt(0, state.screen.Height()-1, state.ewsgben.TotalRowContent()) -} - -func (state *State) displayStages() { - state.screen.BoldPrintAt(0, 2, state.essgben.Headings()) - - // print out the data - max_rows := state.screen.Height() - 3 - row_content := state.essgben.RowContent(max_rows) - - // print out rows - for k := range row_content { - y := 3 + k - state.screen.PrintAt(0, y, row_content[k]) - } - // print out empty rows - for k := len(row_content); k < (state.screen.Height() - 3); k++ { - y := 3 + k - if y < state.screen.Height()-1 { - state.screen.PrintAt(0, y, state.essgben.EmptyRowContent()) - } - } - - // print out the totals at the bottom - state.screen.BoldPrintAt(0, state.screen.Height()-1, state.essgben.TotalRowContent()) -} - - -// do we want to show all p_s data? -func (state State) WantRelativeStats() bool { - return state.want_relative_stats -} - -// set if we want data from when we started/reset stats. -func (state *State) SetWantRelativeStats(want_relative_stats bool) { - state.want_relative_stats = want_relative_stats - - state.fsbi.SetWantRelativeStats(want_relative_stats) - state.tlwsbt.SetWantRelativeStats(state.want_relative_stats) - state.tiwsbt.SetWantRelativeStats(state.want_relative_stats) - state.ewsgben.SetWantRelativeStats(state.want_relative_stats) - state.essgben.SetWantRelativeStats(state.want_relative_stats) -} - -// if there's a better way of doing this do it better ... -func now_hhmmss() string { - t := time.Now() - return fmt.Sprintf("%2d:%02d:%02d", t.Hour(), t.Minute(), t.Second()) -} - -// record the latest screen size -func (state *State) ScreenSetSize(width, height int) { - state.screen.SetSize(width, height) -} - -// clean up screen and disconnect database -func (state *State) Cleanup() { - state.screen.Close() - if state.dbh != nil { - state.setup_instruments.RestoreConfiguration() - _ = state.dbh.Close() - } -} diff --git a/version/version.go b/version/version.go index cf77d28..16782b6 100644 --- a/version/version.go +++ b/version/version.go @@ -2,7 +2,7 @@ package version const ( - version = "0.3.2" + version = "0.3.3" ) // return the current application version