steps

package
v0.56.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 19, 2024 License: BSD-2-Clause, BSD-3-Clause, PostgreSQL Imports: 25 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var CheckPlatform = &s.Step{
	ID:          "check_platform",
	Description: "Check whether this platform is supported by pganalyze guided setup",
	Check: func(state *s.SetupState) (bool, error) {
		hostInfo, err := host.Info()
		if err != nil {
			return false, err
		}
		state.OperatingSystem = hostInfo.OS
		state.Platform = hostInfo.Platform
		state.PlatformFamily = hostInfo.PlatformFamily
		state.PlatformVersion = hostInfo.PlatformVersion

		platVerNum, err := strconv.ParseFloat(state.PlatformVersion, 32)
		if err != nil {
			return false, fmt.Errorf("could not parse current platform version: %s / version %s", state.Platform, state.PlatformVersion)
		}

		if state.Platform == "ubuntu" {
			if platVerNum < 14.04 {
				return false, errors.New("Ubuntu versions older than 14.04 are not supported")
			}
		} else if state.Platform == "debian" {
			if platVerNum < 10.0 {
				return false, errors.New("Debian versions older than 10 are not supported")
			}
		} else {
			return false, fmt.Errorf("the current platform (%s) is not currently supported; please contact support", state.Platform)
		}

		return true, nil
	},
}
View Source
var CheckPostgresVersion = &s.Step{
	ID:          "check_postgres_version",
	Description: "Check whether this Postgres version is supported by pganalyze guided setup",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow("SELECT current_setting('server_version'), current_setting('server_version_num')::integer")
		if err != nil {
			return false, err
		}
		state.PGVersionStr = row.GetString(0)
		state.PGVersionNum = row.GetInt(1)

		if state.PGVersionNum < 100000 {
			return false, fmt.Errorf("not supported for Postgres versions older than 10; found %s", state.PGVersionStr)
		}
		return true, nil
	},
}
View Source
var CheckReplicationStatus = &s.Step{
	ID:          "check_replication_status",
	Description: "Check whether the database is a replica, which is currently unsupported by pganalyze guided setup",
	Check: func(state *s.SetupState) (bool, error) {
		result, err := state.QueryRunner.QueryRow("SELECT pg_is_in_recovery()")
		if err != nil {
			return false, err
		}
		isInRecovery := result.GetBool(0)

		if isInRecovery {
			return false, errors.New("Postgres server is a replica; this is currently not supported")
		}
		return true, nil
	},
}
View Source
var CheckRestartNeeded = &s.Step{
	ID:          "check_restart_needed",
	Description: "Check whether a Postgres restart will be necessary in a future step to install the collector",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow(
			`SELECT
current_setting('shared_preload_libraries') LIKE '%pg_stat_statements%',
current_setting('shared_preload_libraries') LIKE '%auto_explain%'`,
		)
		if err != nil {
			return false, err
		}
		hasPgss := row.GetBool(0)
		hasAutoExplain := row.GetBool(1)
		if !hasPgss {
			state.Log(
				`
NOTICE: A Postgres restart will be required to set up query performance monitoring.
A prompt will ask to confirm the restart before this guided setup performs it.
`,
			)
		} else if !hasAutoExplain {
			state.Log(
				`
NOTICE: A Postgres restart will not be required to set up query performance monitoring.

However, a restart *will* be required for the recommended setup of the Automated EXPLAIN
feature. You can still use the alternative log-based setup to explore the feature without
having to restart Postgres.
`,
			)
		} else {
			state.Log(
				`
NOTICE: A Postgres restart will *not* be required to set up any features.

Your system is ready to configure query performance monitoring, Log Insights, and
Automated EXPLAIN.
`,
			)
		}
		if state.Inputs.Scripted {
			return true, nil
		}

		var doSetup bool
		err = survey.AskOne(&survey.Confirm{
			Message: "Continue with setup?",

			Default: hasPgss && hasAutoExplain,
		}, &doSetup)
		if err != nil {
			return false, err
		}
		if !doSetup {
			return false, errors.New("setup aborted")
		}
		return true, nil
	},
}
View Source
var ConfigureLogMinDurationStatement = &s.Step{
	ID:          "li_ensure_supported_log_min_duration_statement",
	Kind:        state.LogInsightsStep,
	Description: "Ensure the log_min_duration_statement setting in Postgres is supported by the collector",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow(`SELECT setting FROM pg_settings WHERE name = 'log_min_duration_statement'`)
		if err != nil {
			return false, err
		}

		lmdsVal := row.GetInt(0)
		needsUpdate := !isSupportedLmds(lmdsVal) ||
			(state.Inputs.Scripted &&
				state.Inputs.GUCS.LogMinDurationStatement.Valid &&
				int(state.Inputs.GUCS.LogMinDurationStatement.Int64) != lmdsVal)
		return !needsUpdate, nil
	},
	Run: func(state *s.SetupState) error {
		row, err := state.QueryRunner.QueryRow(`SELECT setting FROM pg_settings WHERE name = 'log_min_duration_statement'`)
		if err != nil {
			return err
		}
		oldVal := fmt.Sprintf("%sms", row.GetString(0))

		var newVal string
		if state.Inputs.Scripted {
			if !state.Inputs.GUCS.LogMinDurationStatement.Valid {
				return errors.New("log_min_duration_statement not provided and current value is unsupported")
			}
			newValNum := int(state.Inputs.GUCS.LogMinDurationStatement.Int64)
			if !isSupportedLmds(newValNum) {
				return fmt.Errorf("log_min_duration_statement provided as unsupported value '%d'", newValNum)
			}
			newVal = strconv.Itoa(newValNum)
		} else {
			err = survey.AskOne(&survey.Input{
				Message: fmt.Sprintf(
					"Setting 'log_min_duration_statement' is set to '%s', below supported threshold of 10ms; enter supported value in ms or -1 to disable (will be saved to Postgres):",
					oldVal,
				),
			}, &newVal, survey.WithValidator(util.ValidateLogMinDurationStatement))
			if err != nil {
				return err
			}
		}

		return util.ApplyConfigSetting("log_min_duration_statement", newVal, state.QueryRunner)
	},
}
View Source
var ConfirmAutoExplainAvailable = &s.Step{
	Kind:        s.AutomatedExplainStep,
	ID:          "aemod_check_auto_explain_available",
	Description: "Confirm the auto_explain contrib module is available",
	Check: func(state *s.SetupState) (bool, error) {
		logExplain, err := util.UsingLogExplain(state.CurrentSection)
		if err != nil || logExplain {
			return logExplain, err
		}
		err = state.QueryRunner.Exec("LOAD 'auto_explain'")
		if err != nil {
			if strings.Contains(err.Error(), "No such file or directory") {
				return false, nil
			}

			return false, err
		}
		return true, err
	},
	Run: func(state *s.SetupState) error {
		return errors.New("contrib module auto_explain is not available")
	},
}
View Source
var ConfirmAutomatedExplainMode = &s.Step{
	Kind:        s.AutomatedExplainStep,
	ID:          "ae_confirm_automated_explain_mode",
	Description: "Confirm whether to implement Automated EXPLAIN via the recommended auto_explain module or the alternative log-based EXPLAIN",
	Check: func(state *s.SetupState) (bool, error) {
		return state.CurrentSection.HasKey("enable_log_explain"), nil
	},
	Run: func(state *s.SetupState) error {
		var useLogBased bool
		if state.Inputs.Scripted {
			if !state.Inputs.UseLogBasedExplain.Valid {
				return errors.New("use_log_based_explain not set")
			}
			useLogBased = state.Inputs.UseLogBasedExplain.Bool
		} else {
			var optIdx int
			err := survey.AskOne(&survey.Select{
				Message: "Select automated EXPLAIN mechanism to use (will be saved to collector config):",
				Help:    "Learn more about the options at https://pganalyze.com/docs/explain/setup",
				Options: []string{"auto_explain (recommended)", "Log-based EXPLAIN"},
			}, &optIdx)
			if err != nil {
				return err
			}
			useLogBased = optIdx == 1
		}

		_, err := state.CurrentSection.NewKey("enable_log_explain", strconv.FormatBool(useLogBased))
		if err != nil {
			return err
		}
		return state.SaveConfig()
	},
}
View Source
var ConfirmEmitTestExplain = &s.Step{
	Kind:        s.AutomatedExplainStep,
	ID:          "ae_confirm_emit_test_explain",
	Description: "Invoke the collector EXPLAIN test to generate an EXPLAIN plan based on pg_sleep",
	Check: func(state *s.SetupState) (bool, error) {
		return state.DidTestExplainCommand ||
			state.Inputs.Scripted && (!state.Inputs.ConfirmRunTestExplainCommand.Valid || !state.Inputs.ConfirmRunTestExplainCommand.Bool), nil
	},
	Run: func(state *s.SetupState) error {
		var doTestCommand bool
		if state.Inputs.Scripted {
			doTestCommand = state.Inputs.ConfirmRunTestExplainCommand.Valid && state.Inputs.ConfirmRunTestExplainCommand.Bool
		} else {
			err := survey.AskOne(&survey.Confirm{
				Message: "Issue pg_sleep statement on server to test EXPLAIN configuration",
				Help:    "Learn more about pg_sleep here: https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-DELAY",
				Default: false,
			}, &doTestCommand)
			if err != nil {
				return err
			}
			state.Inputs.ConfirmRunTestExplainCommand = null.BoolFrom(doTestCommand)
		}
		if !doTestCommand {
			return nil
		}

		state.Log("")
		args := []string{"--test-explain", fmt.Sprintf("--config=%s", state.ConfigFilename)}
		cmd := exec.Command("pganalyze-collector", args...)
		var stdOut bytes.Buffer
		cmd.Stdout = &stdOut
		cmd.Stderr = os.Stderr
		err := cmd.Run()
		if err != nil {
			addlInfo := err.Error()
			stdOutStr := stdOut.String()
			if stdOutStr != "" {
				addlInfo = addlInfo + "\n" + stdOutStr
			}
			return fmt.Errorf("test explain command failed: %s", addlInfo)
		}
		state.Log("")

		state.DidTestExplainCommand = true
		return nil
	},
}
View Source
var ConfirmPgssAvailable = &s.Step{
	ID:          "check_pgss_available",
	Description: "Confirm the pg_stat_statements extension is ready to be installed",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow(
			fmt.Sprintf(
				"SELECT true FROM pg_available_extensions WHERE name = 'pg_stat_statements'",
			),
		)
		if err == query.ErrNoRows {
			return false, nil
		} else if err != nil {
			return false, err
		}
		return row.GetBool(0), nil
	},
	Run: func(state *s.SetupState) error {
		return errors.New("contrib extension pg_stat_statements is not available")
	},
}
View Source
var ConfirmRestartPostgres = &s.Step{
	ID:          "confirm_restart_postgres",
	Description: "Confirm whether Postgres should be restarted to have pending configuration changes take effect",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow("SELECT COUNT(*) FROM pg_settings WHERE pending_restart;")
		if err != nil {
			return false, err
		}
		return row.GetInt(0) == 0, nil
	},
	Run: func(state *s.SetupState) error {
		rows, err := state.QueryRunner.Query("SELECT name FROM pg_settings WHERE pending_restart")
		if err != nil {
			return err
		}
		var pendingSettings []string
		for _, row := range rows {
			pendingSettings = append(pendingSettings, row.GetString(0))
		}

		pendingList := util.JoinWithAnd(pendingSettings)
		var restartNow bool
		if state.Inputs.Scripted {
			if !state.Inputs.ConfirmPostgresRestart.Valid || !state.Inputs.ConfirmPostgresRestart.Bool {
				return fmt.Errorf("confirm_postgres_restart flag not set but Postgres restart required for settings %s", pendingList)
			}
			restartNow = state.Inputs.ConfirmPostgresRestart.Bool
		} else {
			err = survey.AskOne(&survey.Confirm{
				Message: fmt.Sprintf("WARNING: Postgres must be restarted for changes to %s to take effect; restart Postgres now?", pendingList),
				Default: false,
			}, &restartNow)
			if err != nil {
				return err
			}

			if !restartNow {
				return nil
			}

			err = survey.AskOne(&survey.Confirm{
				Message: "WARNING: Your database will be restarted. Are you sure?",
				Default: false,
			}, &restartNow)
			if err != nil {
				return err
			}
		}

		if !restartNow {
			return nil
		}

		return service.RestartPostgres(state)
	},
}
View Source
var ConfirmRunTestCommand = &s.Step{
	ID:          "confirm_run_test_command",
	Description: "Invoke the collector self-test to verify the installation",
	Check: func(state *s.SetupState) (bool, error) {
		return state.DidTestCommand ||
			state.Inputs.Scripted && (!state.Inputs.ConfirmRunTestCommand.Valid || !state.Inputs.ConfirmRunTestCommand.Bool), nil
	},
	Run: func(state *s.SetupState) error {
		var doTestCommand bool
		if state.Inputs.Scripted {
			doTestCommand = state.Inputs.ConfirmRunTestCommand.Valid && state.Inputs.ConfirmRunTestCommand.Bool
		} else {
			err := survey.AskOne(&survey.Confirm{
				Message: "The collector is now ready to begin monitoring. Run test command and reload collector configuration if successful?",
				Default: false,
			}, &doTestCommand)
			if err != nil {
				return err
			}
			state.Inputs.ConfirmRunTestCommand = null.BoolFrom(doTestCommand)
		}
		if !doTestCommand {
			return nil
		}

		state.Log("")
		args := []string{"--test", "--reload", fmt.Sprintf("--config=%s", state.ConfigFilename)}
		extraArgsStr := os.Getenv("PGA_SETUP_COLLECTOR_TEST_EXTRA_ARGS")
		if extraArgsStr != "" {
			extraArgs := strings.Split(extraArgsStr, " ")
			args = append(args, extraArgs...)
		}
		cmd := exec.Command("pganalyze-collector", args...)
		var stdOut bytes.Buffer
		cmd.Stdout = &stdOut
		cmd.Stderr = os.Stderr
		err := cmd.Run()
		if err != nil {
			addlInfo := err.Error()
			stdOutStr := stdOut.String()
			if stdOutStr != "" {
				addlInfo = addlInfo + "\n" + stdOutStr
			}
			return fmt.Errorf("test command failed: %s", addlInfo)
		}
		state.Log("")

		state.DidTestCommand = true
		return nil
	},
}
View Source
var ConfirmSetUpAutoExplain = &s.Step{
	ID: "li_confirm_set_up_auto_explain",

	Kind:        state.LogInsightsStep,
	Description: "Confirm whether to set up the optional Automated EXPLAIN feature",
	Check: func(state *s.SetupState) (bool, error) {

		if state.Inputs.ConfirmSetUpAutomatedExplain.Valid {
			return true, nil
		}

		if !state.CurrentSection.HasKey("enable_log_explain") {
			return false, nil
		}

		isLogExplainKey, err := state.CurrentSection.GetKey("enable_log_explain")
		if err != nil {
			return false, err
		}
		isLogExplain, err := isLogExplainKey.Bool()
		if err != nil {
			return false, err
		}
		if isLogExplain {
			return true, nil
		}

		spl, err := util.GetPendingSharedPreloadLibraries(state.QueryRunner)
		if err != nil {
			return false, err
		}
		return strings.Contains(spl, "auto_explain"), nil
	},
	Run: func(state *s.SetupState) error {
		if state.Inputs.Scripted {
			return errors.New("skip_auto_explain value must be specified")
		}

		state.Log(`
Log Insights and query performance setup is almost complete. You can complete it
now, or proceed to configuring the optional Automated EXPLAIN feature. Automated
EXPLAIN will require either setting up the auto_explain module (recommended) or
creating helper functions in all monitored databases. The auto_explain module has
minimal impact on most query workloads with our recommended settings; we will review
these during setup.

Learn more at https://pganalyze.com/postgres-explain
`)
		var setUpExplain bool
		err := survey.AskOne(&survey.Confirm{
			Message: "Proceed to configuring optional Automated EXPLAIN feature?",
			Default: false,
		}, &setUpExplain)
		if err != nil {
			return err
		}
		state.Inputs.ConfirmSetUpAutomatedExplain = null.BoolFrom(setUpExplain)

		return nil
	},
}
View Source
var ConfirmSetUpLogInsights = &s.Step{
	ID:          "confirm_set_up_log_insights",
	Description: "Confirm whether to set up the optional Log Insights feature",
	Check: func(state *s.SetupState) (bool, error) {
		return state.Inputs.ConfirmSetUpLogInsights.Valid || state.PGAnalyzeSection.HasKey("db_log_location"), nil
	},
	Run: func(state *s.SetupState) error {
		if state.Inputs.Scripted {
			return errors.New("skip_log_insights value must be specified")
		}
		state.Log(`
Basic setup is almost complete. You can complete it now, or proceed to
configuring the optional Log Insights feature. Log Insights will require
specifying your database log file (we may be able to detect this), and
may require changes to some logging-related settings.

Setting up Log Insights is required for the Automated EXPLAIN feature.

Learn more at https://pganalyze.com/log-insights
`)
		var setUpLogInsights bool
		err := survey.AskOne(&survey.Confirm{
			Message: "Proceed to configuring optional Log Insights feature?",
			Default: false,
		}, &setUpLogInsights)
		if err != nil {
			return err
		}
		state.Inputs.ConfirmSetUpLogInsights = null.BoolFrom(setUpLogInsights)

		return nil
	},
}
View Source
var ConfirmSuperuserConnection = &s.Step{
	ID:          "confirm_superuser_connection",
	Description: "Confirm the Postgres superuser connection to use only for this guided setup session",
	Check: func(state *s.SetupState) (bool, error) {
		if state.QueryRunner == nil {
			return false, nil
		}
		err := state.QueryRunner.PingSuper()
		return err == nil, err
	},
	Run: func(state *s.SetupState) error {
		localPgs, err := discoverLocalPgFromUnixSockets()
		if err != nil {
			return err
		}
		var selectedPg LocalPostgres
		if len(localPgs) == 0 {
			return errors.New("failed to find a running local Postgres install")
		} else if len(localPgs) > 1 {
			return errors.New("found multiple local Postgres installs; this is not supported for guided setup")
		} else {
			selectedPg = localPgs[0]
		}
		if state.Inputs.Scripted {
			if selectedPg.Port != 0 {

				if (state.Inputs.PGSetupConnPort.Valid && int(state.Inputs.PGSetupConnPort.Int64) != selectedPg.Port) ||
					(state.Inputs.PGSetupConnSocketDir.Valid && state.Inputs.PGSetupConnSocketDir.String != selectedPg.SocketDir) {

					selectedPg = LocalPostgres{}
				}
			} else {
				if !state.Inputs.PGSetupConnPort.Valid {
					return errors.New("no port specified for setup Postgres connection")
				}
				for _, pg := range localPgs {
					if int(state.Inputs.PGSetupConnPort.Int64) == pg.Port &&
						(!state.Inputs.PGSetupConnSocketDir.Valid ||
							state.Inputs.PGSetupConnSocketDir.String == pg.SocketDir) {
						selectedPg = pg
						break
					}
				}
			}
			if selectedPg.Port == 0 {
				var portStr string
				if state.Inputs.PGSetupConnPort.Valid {
					portStr = " on " + strconv.Itoa(int(state.Inputs.PGSetupConnPort.Int64))
				}
				var socketDirStr string
				if state.Inputs.PGSetupConnSocketDir.Valid {
					socketDirStr = " in " + state.Inputs.PGSetupConnSocketDir.String
				}

				return fmt.Errorf("no Postgres server found listening%s%s", portStr, socketDirStr)
			}
		} else {
			if selectedPg.Port == 0 {
				var opts []string
				for _, localPg := range localPgs {
					opts = append(opts, fmt.Sprintf("port %d in socket dir %s", localPg.Port, localPg.SocketDir))
				}
				var selectedIdx int
				err := survey.AskOne(&survey.Select{
					Message: "Found several Postgres installations; please select one",
					Options: opts,
				}, &selectedIdx)
				if err != nil {
					return err
				}
				selectedPg = localPgs[selectedIdx]
			}
		}

		var pgSuperuser string
		if state.Inputs.Scripted {
			if !state.Inputs.PGSetupConnUser.Valid {
				return errors.New("no user specified for setup Postgres connection")
			}
			pgSuperuser = state.Inputs.PGSetupConnUser.String
		} else {
			err = survey.AskOne(&survey.Select{
				Message: "Select Postgres superuser to connect as for initial setup:",
				Help:    "We will create a separate, restricted monitoring user for the collector later",
				Options: []string{"postgres", "another user..."},
			}, &pgSuperuser)
			if err != nil {
				return err
			}
			if pgSuperuser != "postgres" {
				err = survey.AskOne(&survey.Input{
					Message: "Enter Postgres superuser to connect as for initial setup:",
					Help:    "We will create a separate, restricted monitoring user for the collector later",
				}, &pgSuperuser, survey.WithValidator(survey.Required))
				if err != nil {
					return err
				}
			}
		}

		state.QueryRunner = query.NewRunner(pgSuperuser, selectedPg.SocketDir, selectedPg.Port)

		return nil
	},
}
View Source
var EnablePgssInSpl = &s.Step{
	ID:          "ensure_pgss_in_spl",
	Description: "Ensure the pg_stat_statements extension is included in the shared_preload_libraries setting in Postgres",
	Check: func(state *s.SetupState) (bool, error) {
		spl, err := util.GetPendingSharedPreloadLibraries(state.QueryRunner)
		if err != nil {
			return false, err
		}

		return strings.Contains(spl, "pg_stat_statements"), nil
	},
	Run: func(state *s.SetupState) error {
		var doAdd bool
		if state.Inputs.Scripted {
			if !state.Inputs.EnsurePgStatStatementsLoaded.Valid || !state.Inputs.EnsurePgStatStatementsLoaded.Bool {
				return errors.New("enable_pg_stat_statements flag not set but pg_stat_statements not in shared_preload_libraries")
			}
			doAdd = state.Inputs.EnsurePgStatStatementsLoaded.Bool
		} else {
			err := survey.AskOne(&survey.Confirm{
				Message: "Add pg_stat_statements to shared_preload_libraries (will be saved to Postgres--requires restart in a later step)?",
				Default: false,
				Help:    "Postgres will have to be restarted in a later step to apply this configuration change; learn more about shared_preload_libraries here: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SHARED-PRELOAD-LIBRARIES",
			}, &doAdd)
			if err != nil {
				return err
			}
		}
		if !doAdd {
			return nil
		}

		existingSpl, err := util.GetPendingSharedPreloadLibraries(state.QueryRunner)
		if err != nil {
			return err
		}

		var newSpl string
		if existingSpl == "" {
			newSpl = "pg_stat_statements"
		} else {
			newSpl = existingSpl + ",pg_stat_statements"
		}
		return util.ApplyConfigSetting("shared_preload_libraries", newSpl, state.QueryRunner)
	},
}
View Source
var EnsureAutoExplainInSpl = &s.Step{
	Kind:        s.AutomatedExplainStep,
	ID:          "aemod_ensure_auto_explain_in_spl",
	Description: "Ensure the auto_explain module is included in the shared_preload_libraries setting in Postgres",
	Check: func(state *s.SetupState) (bool, error) {
		logExplain, err := util.UsingLogExplain(state.CurrentSection)
		if err != nil || logExplain {
			return logExplain, err
		}
		spl, err := util.GetPendingSharedPreloadLibraries(state.QueryRunner)
		if err != nil {
			return false, err
		}
		return strings.Contains(spl, "auto_explain"), nil
	},
	Run: func(state *s.SetupState) error {
		var doAdd bool
		if state.Inputs.Scripted {
			if !state.Inputs.EnsureAutoExplainLoaded.Valid || !state.Inputs.EnsureAutoExplainLoaded.Bool {
				return errors.New("enable_auto_explain flag not set but auto_explain configuration selected")
			}
			doAdd = state.Inputs.EnsureAutoExplainLoaded.Bool
		} else {
			err := survey.AskOne(&survey.Confirm{
				Message: "Add auto_explain to shared_preload_libraries (will be saved to Postgres--requires restart in a later step)?",
				Default: false,
				Help:    "Postgres will have to be restarted in a later step to apply this configuration change; learn more about Automated EXPLAIN at https://pganalyze.com/postgres-explain",
			}, &doAdd)
			if err != nil {
				return err
			}
		}
		if !doAdd {
			return nil
		}

		existingSpl, err := util.GetPendingSharedPreloadLibraries(state.QueryRunner)
		if err != nil {
			return err
		}
		var newSpl string
		if existingSpl == "" {
			newSpl = "auto_explain"
		} else {
			newSpl = existingSpl + ",auto_explain"
		}
		return util.ApplyConfigSetting("shared_preload_libraries", newSpl, state.QueryRunner)
	},
}
View Source
var EnsureLogExplainHelpers = &s.Step{
	Kind:        s.AutomatedExplainStep,
	ID:          "aelog_ensure_log_explain_helpers",
	Description: "Ensure EXPLAIN helper functions for log-based EXPLAIN exist in all monitored Postgres databases",
	Check: func(state *s.SetupState) (bool, error) {
		logExplain, err := util.UsingLogExplain(state.CurrentSection)
		if err != nil {
			return false, err
		}
		if !logExplain {
			return true, nil
		}
		monitoredDBs, err := getMonitoredDBs(state)
		if err != nil {
			return false, err
		}

		for _, db := range monitoredDBs {
			dbRunner := state.QueryRunner.InDB(db)
			isValid, err := util.ValidateHelperFunction(util.ExplainHelper, dbRunner)
			if !isValid || err != nil {
				return isValid, err
			}
		}
		return true, nil
	},
	Run: func(state *s.SetupState) error {
		var doCreate bool
		if state.Inputs.Scripted {
			if !state.Inputs.EnsureLogExplainHelpers.Valid || !state.Inputs.EnsureLogExplainHelpers.Bool {
				return errors.New("create_explain_helper flag not set and helper function does not exist or does not match expected signature on all monitored databases")
			}
			doCreate = state.Inputs.EnsureLogExplainHelpers.Bool
		} else {
			err := survey.AskOne(&survey.Confirm{
				Message: "Create (or update) EXPLAIN helper function in each monitored database (will be saved to Postgres)?",
				Default: false,
			}, &doCreate)
			if err != nil {
				return err
			}
		}

		if !doCreate {
			return nil
		}
		monitoredDBs, err := getMonitoredDBs(state)
		if err != nil {
			return err
		}
		for _, db := range monitoredDBs {
			err := createHelperInDB(state, db)
			if err != nil {
				return err
			}
		}
		return nil
	},
}
View Source
var EnsureMonitoringUser = &s.Step{
	ID:          "ensure_monitoring_user",
	Description: "Ensure the monitoring user (db_user in the collector config file) exists in Postgres",
	Check: func(state *s.SetupState) (bool, error) {
		pgaUserKey, err := state.CurrentSection.GetKey("db_username")
		if err != nil {
			return false, err
		}
		pgaUser := pgaUserKey.String()

		var result query.Row
		result, err = state.QueryRunner.QueryRow(fmt.Sprintf("SELECT true FROM pg_user WHERE usename = %s", pq.QuoteLiteral(pgaUser)))
		if err == query.ErrNoRows {
			return false, nil
		} else if err != nil {
			return false, err
		}
		return result.GetBool(0), nil
	},
	Run: func(state *s.SetupState) error {
		pgaUserKey, err := state.CurrentSection.GetKey("db_username")
		if err != nil {
			return err
		}
		pgaUser := pgaUserKey.String()

		var doCreateUser bool
		if state.Inputs.Scripted {
			if !state.Inputs.EnsureMonitoringUser.Valid ||
				!state.Inputs.EnsureMonitoringUser.Bool {
				return fmt.Errorf("create_monitoring_user flag not set and specified monitoring user %s does not exist", pgaUser)
			}
			doCreateUser = state.Inputs.EnsureMonitoringUser.Bool
		} else {
			err = survey.AskOne(&survey.Confirm{
				Message: fmt.Sprintf("User %s does not exist in Postgres; create user (will be saved to Postgres)?", pgaUser),
				Help:    "If you skip this step, create the user manually before proceeding",
				Default: false,
			}, &doCreateUser)
			if err != nil {
				return err
			}
		}

		if !doCreateUser {
			return nil
		}

		return state.QueryRunner.Exec(
			fmt.Sprintf(
				"CREATE USER %s CONNECTION LIMIT 5",
				pq.QuoteIdentifier(pgaUser),
			),
		)
	},
}
View Source
var EnsureMonitoringUserPassword = &s.Step{
	ID:          "ensure_monitoring_user_password",
	Description: "Ensure the monitoring user password in Postgres matches db_password in the collector config file",
	Check: func(state *s.SetupState) (bool, error) {

		cfg, err := config.Read(
			&mainUtil.Logger{Destination: log.New(os.Stderr, "", 0)},
			state.ConfigFilename,
		)
		if err != nil {
			return false, err
		}
		if len(cfg.Servers) != 1 {
			return false, fmt.Errorf("expected one server in config; found %d", len(cfg.Servers))
		}
		serverCfg := cfg.Servers[0]
		pqStr, err := serverCfg.GetPqOpenString("", "")
		if err != nil {
			return false, err
		}
		conn, err := sql.Open("postgres", pqStr)
		err = conn.Ping()
		if err != nil {
			isAuthErr := strings.Contains(err.Error(), "authentication failed")
			if isAuthErr {
				return false, nil
			}
			return false, err
		}

		return true, nil

	},
	Run: func(state *s.SetupState) error {
		pgaUserKey, err := state.CurrentSection.GetKey("db_username")
		if err != nil {
			return err
		}
		pgaUser := pgaUserKey.String()
		pgaPasswdKey, err := state.CurrentSection.GetKey("db_password")
		if err != nil {
			return err
		}
		pgaPasswd := pgaPasswdKey.String()

		var doPasswdUpdate bool
		if state.Inputs.Scripted {
			if !state.Inputs.EnsureMonitoringPassword.Valid {
				return errors.New("update_monitoring_password flag not set and cannot log in with current credentials")
			}
			doPasswdUpdate = state.Inputs.EnsureMonitoringPassword.Bool
		} else {
			err = survey.AskOne(&survey.Confirm{
				Message: fmt.Sprintf("Update password for user %s with configured value (will be saved to Postgres)?", pgaUser),
				Help:    "If you skip this step, ensure the password matches before proceeding",
			}, &doPasswdUpdate)
			if err != nil {
				return err
			}
		}

		if !doPasswdUpdate {
			return nil
		}
		err = state.QueryRunner.Exec(
			fmt.Sprintf(
				"SET log_statement = none; ALTER USER %s WITH ENCRYPTED PASSWORD %s",
				pq.QuoteIdentifier(pgaUser),
				pq.QuoteLiteral(pgaPasswd),
			),
		)
		return err
	},
}
View Source
var EnsureMonitoringUserPermissions = &s.Step{
	ID:          "ensure_monitoring_user_permissions",
	Description: "Ensure the monitoring user has sufficient permissions in Postgres for access to queries and monitoring metadata",
	Check: func(state *s.SetupState) (bool, error) {
		pgaUserKey, err := state.CurrentSection.GetKey("db_username")
		if err != nil {
			return false, err
		}
		pgaUser := pgaUserKey.String()

		row, err := state.QueryRunner.QueryRow(
			fmt.Sprintf(
				"SELECT usesuper OR pg_has_role(usename, 'pg_monitor', 'usage') FROM pg_user WHERE usename = %s",
				pq.QuoteLiteral(pgaUser),
			),
		)
		if err == query.ErrNoRows {
			return false, nil
		} else if err != nil {
			return false, err
		}

		return row.GetBool(0), nil
	},
	Run: func(state *s.SetupState) error {
		pgaUserKey, err := state.CurrentSection.GetKey("db_username")
		if err != nil {
			return err
		}
		pgaUser := pgaUserKey.String()

		var doGrant bool
		if state.Inputs.Scripted {
			if !state.Inputs.EnsureMonitoringPermissions.Valid || !state.Inputs.EnsureMonitoringPermissions.Bool {
				return errors.New("set_up_monitoring_user flag not set and monitoring user does not have adequate permissions")
			}
			doGrant = state.Inputs.EnsureMonitoringPermissions.Bool
		} else {
			err = survey.AskOne(&survey.Confirm{
				Message: fmt.Sprintf("Grant role pg_monitor to user %s (will be saved to Postgres)?", pgaUser),
				Help:    "Learn more about pg_monitor here: https://www.postgresql.org/docs/current/default-roles.html",
			}, &doGrant)
			if err != nil {
				return err
			}
		}
		if !doGrant {
			return nil
		}

		return state.QueryRunner.Exec(
			fmt.Sprintf(
				"GRANT pg_monitor to %s",
				pq.QuoteIdentifier(pgaUser),
			),
		)
	},
}
View Source
var EnsurePganalyzeSchema = &s.Step{
	ID:          "ensure_pganalyze_schema",
	Description: "Ensure the pganalyze schema exists and db_user in the collector config file has USAGE privilege on it",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow("SELECT COUNT(*) FROM pg_namespace WHERE nspname = 'pganalyze'")
		if err != nil {
			return false, err
		}
		count := row.GetInt(0)
		if count != 1 {
			return false, nil
		}
		userKey, err := state.CurrentSection.GetKey("db_username")
		if err != nil {
			return false, err
		}
		pgaUser := userKey.String()
		row, err = state.QueryRunner.QueryRow(fmt.Sprintf("SELECT has_schema_privilege(%s, 'pganalyze', 'USAGE')", pq.QuoteLiteral(pgaUser)))
		if err != nil {
			return false, err
		}
		hasUsage := row.GetBool(0)
		if !hasUsage {
			return false, nil
		}

		return true, nil
	},
	Run: func(state *s.SetupState) error {
		var doSetup bool
		if state.Inputs.Scripted {
			if !state.Inputs.EnsureHelperFunctions.Valid || !state.Inputs.EnsureHelperFunctions.Bool {
				return errors.New("create_helper_functions flag not set and pganalyze schema or helper functions do not exist")
			}
			doSetup = state.Inputs.EnsureHelperFunctions.Bool
		} else {
			err := survey.AskOne(&survey.Confirm{
				Message: "Create pganalyze schema and helper functions (will be saved to Postgres)?",
				Default: false,

				Help: "These helper functions allow the collector to monitor database statistics without being able to read your data; learn more here: https://github.com/pganalyze/collector/#setting-up-a-restricted-monitoring-user",
			}, &doSetup)
			if err != nil {
				return err
			}
		}

		if !doSetup {
			return nil
		}

		userKey, err := state.CurrentSection.GetKey("db_username")
		if err != nil {
			return err
		}
		pgaUser := userKey.String()

		return state.QueryRunner.Exec(
			fmt.Sprintf(
				`CREATE SCHEMA IF NOT EXISTS pganalyze; GRANT USAGE ON SCHEMA pganalyze TO %s;`,
				pq.QuoteIdentifier(pgaUser),
			),
		)
	},
}
View Source
var EnsurePgssExtInstalled = &s.Step{
	ID:          "ensure_pgss_ext_installed",
	Description: "Ensure the pg_stat_statements extension is installed in Postgres",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow(
			fmt.Sprintf(
				"SELECT extnamespace::regnamespace::text FROM pg_extension WHERE extname = 'pg_stat_statements'",
			),
		)
		if err == query.ErrNoRows {
			return false, nil
		} else if err != nil {
			return false, err
		}
		extNsp := row.GetString(0)
		if extNsp != "public" {
			return false, fmt.Errorf("pg_stat_statements is installed, but in unsupported schema %s; must be installed in 'public'", extNsp)
		}
		return true, nil
	},
	Run: func(state *s.SetupState) error {
		var doCreate bool
		if state.Inputs.Scripted {
			if !state.Inputs.EnsurePgStatStatementsInstalled.Valid || !state.Inputs.EnsurePgStatStatementsInstalled.Bool {
				return errors.New("create_pg_stat_statements flag not set and pg_stat_statements does not exist in primary database")
			}
			doCreate = state.Inputs.EnsurePgStatStatementsInstalled.Bool
		} else {
			err := survey.AskOne(&survey.Confirm{
				Message: "Create extension pg_stat_statements in public schema for query performance monitoring (will be saved to Postgres)?",
				Default: false,
				Help:    "Learn more about pg_stat_statements here: https://www.postgresql.org/docs/current/pgstatstatements.html",
			}, &doCreate)
			if err != nil {
				return err
			}
		}

		if !doCreate {
			return nil
		}
		return state.QueryRunner.Exec("CREATE EXTENSION pg_stat_statements SCHEMA public")
	},
}
View Source
var EnsureRecommendedAutoExplainSettings = &s.Step{
	Kind:        s.AutomatedExplainStep,
	ID:          "aemod_ensure_recommended_settings",
	Description: "Ensure auto_explain settings in Postgres are configured as recommended, if desired",
	Check: func(state *s.SetupState) (bool, error) {
		if state.DidAutoExplainRecommendedSettings ||
			(state.Inputs.EnsureAutoExplainRecommendedSettings.Valid && !state.Inputs.EnsureAutoExplainRecommendedSettings.Bool) {
			return true, nil
		}
		logExplain, err := util.UsingLogExplain(state.CurrentSection)
		if err != nil || logExplain {
			return logExplain, err
		}

		autoExplainGucsQuery := getAutoExplainGUCSQuery(state)
		rows, err := state.QueryRunner.Query(
			autoExplainGucsQuery,
		)
		if err != nil {
			return false, fmt.Errorf("error checking existing settings: %s", err)
		}

		return len(rows) == 0, nil
	},
	Run: func(state *s.SetupState) error {
		var doReview bool
		if state.Inputs.Scripted {
			if state.Inputs.EnsureAutoExplainRecommendedSettings.Valid {
				doReview = state.Inputs.EnsureAutoExplainRecommendedSettings.Bool
			}
		} else {
			err := survey.AskOne(&survey.Confirm{
				Message: "Review auto_explain configuration settings?",
				Default: false,
				Help:    "Optional, but will ensure best balance of monitoring visibility and performance; review these settings at https://pganalyze.com/docs/explain/setup/auto_explain",
			}, &doReview)
			if err != nil {
				return err
			}
			state.Inputs.EnsureAutoExplainRecommendedSettings = null.BoolFrom(doReview)
		}

		if !doReview {
			return nil
		}

		autoExplainGucsQuery := getAutoExplainGUCSQuery(state)
		rows, err := state.QueryRunner.Query(
			autoExplainGucsQuery,
		)
		if err != nil {
			return fmt.Errorf("error checking existing settings: %s", err)
		}
		if len(rows) == 0 {
			state.Log("all auto_explain configuration settings using recommended values")
			state.DidAutoExplainRecommendedSettings = true
			return nil
		}
		settingsToReview := make(map[string]string)
		for _, row := range rows {
			settingsToReview[row.GetString(0)] = row.GetString(1)
		}

		if currValue, ok := settingsToReview["auto_explain.log_timing"]; ok {
			logTiming, err := getLogTimingValue(state, currValue)
			if err != nil {
				return err
			}
			if logTiming != currValue {
				err = util.ApplyConfigSetting("auto_explain.log_timing", logTiming, state.QueryRunner)
				if err != nil {
					return err
				}
			}
		}

		if currValue, ok := settingsToReview["auto_explain.log_analyze"]; ok {
			logAnalyze, err := getLogAnalyzeValue(state, currValue)
			if err != nil {
				return err
			}

			if logAnalyze != currValue {
				err = util.ApplyConfigSetting("auto_explain.log_analyze", logAnalyze, state.QueryRunner)
				if err != nil {
					return err
				}
			}
		}

		row, err := state.QueryRunner.QueryRow("SHOW auto_explain.log_analyze")
		if err != nil {
			return err
		}
		isLogAnalyzeOn := row.GetString(0) == "on"

		if isLogAnalyzeOn {
			if currValue, ok := settingsToReview["auto_explain.log_buffers"]; ok {
				logBuffers, err := getLogBuffersValue(state, currValue)
				if err != nil {
					return err
				}

				if logBuffers != currValue {
					err = util.ApplyConfigSetting("auto_explain.log_buffers", logBuffers, state.QueryRunner)
					if err != nil {
						return err
					}
				}
			}

			if currValue, ok := settingsToReview["auto_explain.log_triggers"]; ok {
				logTriggers, err := getLogTriggersValue(state, currValue)
				if err != nil {
					return err
				}

				if logTriggers != currValue {
					err = util.ApplyConfigSetting("auto_explain.log_triggers", logTriggers, state.QueryRunner)
					if err != nil {
						return err
					}
				}
			}

			if currValue, ok := settingsToReview["auto_explain.log_verbose"]; ok {
				logVerbose, err := getLogVerboseValue(state, currValue)
				if err != nil {
					return err
				}
				if logVerbose != currValue {
					err = util.ApplyConfigSetting("auto_explain.log_verbose", logVerbose, state.QueryRunner)
					if err != nil {
						return err
					}
				}
			}
		}

		if currValue, ok := settingsToReview["auto_explain.log_format"]; ok {
			logFormat, err := getLogFormatValue(state, currValue)
			if err != nil {
				return err
			}

			if logFormat != currValue {
				err = util.ApplyConfigSetting("auto_explain.log_format", logFormat, state.QueryRunner)
				if err != nil {
					return err
				}
			}
		}

		if currValue, ok := settingsToReview["auto_explain.log_min_duration"]; ok {
			logMinDuration, err := getLogMinDurationValue(state, currValue)
			if err != nil {
				return err
			}

			if logMinDuration != currValue {
				err = util.ApplyConfigSetting("auto_explain.log_min_duration", logMinDuration, state.QueryRunner)
				if err != nil {
					return err
				}
			}
		}

		if currValue, ok := settingsToReview["auto_explain.log_nested_statements"]; ok {
			logNested, err := getLogNestedStatements(state, currValue)
			if err != nil {
				return err
			}

			if logNested != currValue {
				err = util.ApplyConfigSetting("auto_explain.log_nested_statements", logNested, state.QueryRunner)
				if err != nil {
					return err
				}
			}
		}
		state.DidAutoExplainRecommendedSettings = true
		return nil
	},
}

N.B.: this needs to happen *after* the Postgres restart so that ALTER SYSTEM recognizes these as valid configuration settings

View Source
var EnsureSupportedLogDuration = &s.Step{
	ID:          "li_ensure_supported_log_duration",
	Kind:        state.LogInsightsStep,
	Description: "Ensure the log_duration setting in Postgres is supported by the collector",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow(`SELECT setting FROM pg_settings WHERE name = 'log_duration'`)
		if err != nil {
			return false, err
		}

		currValue := row.GetString(0)
		needsUpdate := currValue == "on" ||
			(state.Inputs.Scripted && state.Inputs.GUCS.LogDuration.Valid &&
				state.Inputs.GUCS.LogDuration.String != currValue)

		return !needsUpdate, nil
	},
	Run: func(state *s.SetupState) error {
		var turnOffLogDuration bool
		if state.Inputs.Scripted {
			if !state.Inputs.GUCS.LogDuration.Valid {
				return errors.New("log_duration value not provided and current value not supported")
			}
			if state.Inputs.GUCS.LogDuration.String == "on" {
				return errors.New("log_duration provided as unsupported value 'on'")
			}
			turnOffLogDuration = state.Inputs.GUCS.LogDuration.String == "off"
		} else {
			err := survey.AskOne(&survey.Confirm{
				Message: "Setting 'log_duration' is set to unsupported value 'on'; set to 'off' (will be saved to Postgres)?",
				Default: false,
			}, &turnOffLogDuration)
			if err != nil {
				return err
			}
		}
		if !turnOffLogDuration {

			return nil
		}
		return util.ApplyConfigSetting("log_duration", "off", state.QueryRunner)
	},
}
View Source
var EnsureSupportedLogErrorVerbosity = &s.Step{
	ID:          "li_ensure_supported_log_error_verbosity",
	Kind:        state.LogInsightsStep,
	Description: "Ensure the log_error_verbosity setting in Postgres is supported by the collector",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow(`SELECT setting FROM pg_settings WHERE name = 'log_error_verbosity'`)
		if err != nil {
			return false, err
		}

		currVal := row.GetString(0)
		needsUpdate := currVal == "verbose" ||
			(state.Inputs.Scripted &&
				state.Inputs.GUCS.LogErrorVerbosity.Valid &&
				currVal != state.Inputs.GUCS.LogErrorVerbosity.String)

		return !needsUpdate, nil
	},
	Run: func(state *s.SetupState) error {
		var newVal string
		if state.Inputs.Scripted {
			if !state.Inputs.GUCS.LogErrorVerbosity.Valid {
				return errors.New("log_error_verbosity value not provided and current value not supported")
			}
			if state.Inputs.GUCS.LogErrorVerbosity.String == "verbose" {
				return errors.New("log_error_verbosity provided as unsupported value 'verbose'")
			}
			newVal = state.Inputs.GUCS.LogErrorVerbosity.String
		} else {
			err := survey.AskOne(&survey.Select{
				Message: "Setting 'log_error_verbosity' is set to unsupported value 'verbose'; select supported value (will be saved to Postgres):",
				Options: []string{"terse", "default"},
			}, &newVal)
			if err != nil {
				return err
			}
		}

		return util.ApplyConfigSetting("log_error_verbosity", newVal, state.QueryRunner)
	},
}
View Source
var EnsureSupportedLogLinePrefix = &s.Step{
	ID:          "li_ensure_supported_log_line_prefix",
	Kind:        s.LogInsightsStep,
	Description: "Ensure the log_line_prefix setting in Postgres is supported by the collector",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow(`SELECT setting FROM pg_settings WHERE name = 'log_line_prefix'`)
		if err != nil {
			return false, err
		}

		currValue := row.GetString(0)
		needsUpdate := !util.Includes(s.SupportedLogLinePrefixes, currValue) ||
			(state.Inputs.Scripted &&
				state.Inputs.GUCS.LogLinePrefix.Valid &&
				currValue != state.Inputs.GUCS.LogLinePrefix.String)

		return !needsUpdate, nil
	},
	Run: func(state *s.SetupState) error {
		var selectedPrefix string
		if state.Inputs.Scripted {
			if !state.Inputs.GUCS.LogLinePrefix.Valid {
				return errors.New("log_line_prefix not provided and current setting is not supported")
			}
			selectedPrefix = state.Inputs.GUCS.LogLinePrefix.String
			if !util.Includes(s.SupportedLogLinePrefixes, selectedPrefix) {
				return fmt.Errorf("log_line_prefix provided as unsupported value '%s'", selectedPrefix)
			}
		} else {
			row, err := state.QueryRunner.QueryRow(`SELECT setting FROM pg_settings WHERE name = 'log_line_prefix'`)
			if err != nil {
				return err
			}
			oldVal := row.GetString(0)
			var opts []string
			for i, llp := range s.SupportedLogLinePrefixes {
				// N.B.: we quote the options because many prefixes end in whitespace; we need to make that clear
				var opt string
				if i == 0 {
					opt = fmt.Sprintf("'%s' (recommended)", llp)
				} else {
					opt = fmt.Sprintf("'%s'", llp)
				}
				opts = append(opts, opt)
			}
			var prefixIdx int
			err = survey.AskOne(&survey.Select{
				Message: fmt.Sprintf("Setting 'log_line_prefix' is set to unsupported value '%s'; set to (will be saved to Postgres):", oldVal),
				Help:    "Check format specifier reference in Postgres documentation: https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-LOG-LINE-PREFIX",
				Options: opts,
			}, &prefixIdx)
			if err != nil {
				return err
			}
			selectedPrefix = s.SupportedLogLinePrefixes[prefixIdx]
		}
		return util.ApplyConfigSetting("log_line_prefix", pq.QuoteLiteral(selectedPrefix), state.QueryRunner)
	},
}
View Source
var EnsureSupportedLogStatement = &s.Step{
	ID:          "li_ensure_supported_log_statement",
	Kind:        state.LogInsightsStep,
	Description: "Ensure the log_statement setting in Postgres is supported by the collector",
	Check: func(state *s.SetupState) (bool, error) {
		row, err := state.QueryRunner.QueryRow(`SELECT setting FROM pg_settings WHERE name = 'log_statement'`)
		if err != nil {
			return false, err
		}
		currValue := row.GetString(0)
		needsUpdate := currValue == "all" ||
			(state.Inputs.Scripted &&
				state.Inputs.GUCS.LogStatement.Valid &&
				currValue != state.Inputs.GUCS.LogStatement.String)

		return !needsUpdate, nil
	},
	Run: func(state *s.SetupState) error {
		var newVal string
		if state.Inputs.Scripted {
			if !state.Inputs.GUCS.LogStatement.Valid {
				return errors.New("log_statement value not provided and current value not supported")
			}
			if state.Inputs.GUCS.LogStatement.String == "all" {
				return errors.New("log_statement provided as unsupported value 'all'")
			}

			newVal = state.Inputs.GUCS.LogStatement.String
		} else {
			err := survey.AskOne(&survey.Select{
				Message: "Setting 'log_statement' is set to unsupported value 'all'; select supported value (will be saved to Postgres):",
				Options: []string{"none", "ddl", "mod"},
			}, &newVal)
			if err != nil {
				return err
			}
		}

		return util.ApplyConfigSetting("log_statement", newVal, state.QueryRunner)
	},
}
View Source
var SpecifyAPIKey = &s.Step{
	ID:          "specify_api_key",
	Description: "Specify the pganalyze API key (api_key) in the collector config file",
	Check: func(state *s.SetupState) (bool, error) {
		return state.PGAnalyzeSection.HasKey("api_key"), nil
	},
	Run: func(state *s.SetupState) error {
		var apiKey string
		var apiBaseURL string

		if state.Inputs.Settings.APIKey.Valid {
			apiKey = state.Inputs.Settings.APIKey.String
		}
		if state.Inputs.Settings.APIBaseURL.Valid {
			apiBaseURL = state.Inputs.Settings.APIBaseURL.String
		}

		var configWriteConfirmed bool

		if state.Inputs.Scripted {
			if apiKey != "" {
				configWriteConfirmed = true
			} else {
				return errors.New("no api_key setting specified")
			}
		} else if apiKey == "" {
			err := survey.AskOne(&survey.Input{
				Message: "Please enter API key (will be saved to collector config):",
				Help:    "The key can be found on the API keys page for your organization in the pganalyze app",
			}, &apiKey, survey.WithValidator(survey.Required))
			if err != nil {
				return err
			}
			configWriteConfirmed = true
		} else {
			err := survey.AskOne(&survey.Confirm{
				Message: "Save pganalyze API key to collector config?",
				Default: false,
			}, &configWriteConfirmed)
			if err != nil {
				return err
			}
		}
		if !configWriteConfirmed {
			return nil
		}
		_, err := state.PGAnalyzeSection.NewKey("api_key", apiKey)
		if err != nil {
			return err
		}
		if apiBaseURL != "" {
			_, err := state.PGAnalyzeSection.NewKey("api_base_url", apiBaseURL)
			if err != nil {
				return err
			}
		}
		return state.SaveConfig()
	},
}
View Source
var SpecifyDatabases = &s.Step{
	ID:          "specify_databases",
	Description: "Specify database(s) to monitor (db_name) in the collector config file",
	Check: func(state *s.SetupState) (bool, error) {
		hasDb := state.CurrentSection.HasKey("db_name")
		if !hasDb {
			return false, nil
		}
		key, err := state.CurrentSection.GetKey("db_name")
		if err != nil {
			return false, err
		}
		dbs := key.Strings(",")
		if len(dbs) == 0 || dbs[0] == "" {
			return false, nil
		}
		db := dbs[0]

		state.QueryRunner.Database = db
		return true, nil
	},
	Run: func(state *s.SetupState) error {
		rows, err := state.QueryRunner.Query("SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate")
		if err != nil {
			return err
		}
		var dbOpts []string
		for _, row := range rows {
			dbOpts = append(dbOpts, row.GetString(0))
		}

		var dbNames []string
		if state.Inputs.Scripted {
			if !state.Inputs.Settings.DBName.Valid {
				return errors.New("no db_name setting specified")
			}
			dbNameInputs := strings.Split(state.Inputs.Settings.DBName.String, ",")
			for i, dbNameInput := range dbNameInputs {
				trimmed := strings.TrimSpace(dbNameInput)
				if trimmed == "*" {
					dbNames = append(dbNames, trimmed)
				} else {
					for _, opt := range dbOpts {
						if trimmed == opt {
							dbNames = append(dbNames, trimmed)
							break
						}
					}
				}

				if len(dbNames) != i+1 {
					return fmt.Errorf("database %s configured for db_name but not found in Postgres", trimmed)
				}
			}
		} else {
			var primaryDb string
			err = survey.AskOne(&survey.Select{
				Message: "Choose a primary database to monitor (will be saved to collector config):",
				Options: dbOpts,
				Help:    "The collector will connect to this database for monitoring; others can be added next",
			}, &primaryDb)
			if err != nil {
				return err
			}

			dbNames = append(dbNames, primaryDb)
			if len(dbOpts) == 1 {
				var monitorAll bool
				err = survey.AskOne(&survey.Confirm{
					Message: "Monitor all other databases created in the future (will be saved to collector config)?",
					Default: true,
				}, &monitorAll)
				if err != nil {
					return err
				}
				if monitorAll {
					dbNames = append(dbNames, "*")
				}
			} else if len(dbOpts) > 1 {
				var otherDbs []string
				for _, db := range dbOpts {
					if db == primaryDb {
						continue
					}
					otherDbs = append(otherDbs, db)
				}
				var othersOptIdx int
				err = survey.AskOne(&survey.Select{
					Message: "Monitor other databases (will be saved to collector config)?",
					Help:    "The 'all' option will also automatically monitor all future databases created on this server",
					Options: []string{"all other databases (including future ones)", "no other databases", "select databases..."},
				}, &othersOptIdx)
				if err != nil {
					return err
				}
				if othersOptIdx == 0 {
					dbNames = append(dbNames, "*")
				} else if othersOptIdx == 1 {

				} else if othersOptIdx == 2 {
					var otherDbsSelected []string
					err = survey.AskOne(&survey.MultiSelect{
						Message: "Select other databases to monitor (will be saved to collector config):",
						Options: otherDbs,
					}, &otherDbsSelected)
					if err != nil {
						return err
					}
					dbNames = append(dbNames, otherDbsSelected...)
				} else {
					panic(fmt.Sprintf("unexpected other databases selection: %d", othersOptIdx))
				}
			}
		}

		dbNamesStr := strings.Join(dbNames, ",")
		_, err = state.CurrentSection.NewKey("db_name", dbNamesStr)
		if err != nil {
			return err
		}

		return state.SaveConfig()
	},
}
View Source
var SpecifyDbLogLocation = &s.Step{
	ID:          "li_specify_db_log_location",
	Kind:        state.LogInsightsStep,
	Description: "Specify the location of Postgres log files (db_log_location) in the collector config file",
	Check: func(state *s.SetupState) (bool, error) {
		return state.CurrentSection.HasKey("db_log_location"), nil
	},
	Run: func(state *s.SetupState) error {
		var logLocation string
		if state.Inputs.Scripted {
			loc, err := getLogLocationScripted(state)
			if err != nil {
				return err
			}
			logLocation = loc
		} else {
			loc, err := getLogLocationInteractive(state)
			if err != nil {
				return err
			}
			logLocation = loc
		}

		_, err := state.CurrentSection.NewKey("db_log_location", logLocation)
		if err != nil {
			return err
		}
		return state.SaveConfig()
	},
}
View Source
var SpecifyMonitoringUser = &s.Step{
	ID:          "specify_monitoring_user",
	Description: "Specify the monitoring user to connect as (db_username) in the collector config file",
	Check: func(state *s.SetupState) (bool, error) {
		hasUser := state.CurrentSection.HasKey("db_username")
		return hasUser, nil
	},
	Run: func(state *s.SetupState) error {
		var pgaUser string

		if state.Inputs.Scripted {
			if !state.Inputs.Settings.DBUsername.Valid {
				return errors.New("no db_username setting specified")
			}
			pgaUser = state.Inputs.Settings.DBUsername.String
		} else {
			var monitoringUserIdx int
			err := survey.AskOne(&survey.Select{
				Message: "Select Postgres user for the collector to use (will be saved to collector config):",
				Help:    "If the user does not exist, it can be created in a later step",
				Options: []string{"pganalyze (recommended)", "a different user"},
			}, &monitoringUserIdx)
			if err != nil {
				return err
			}

			if monitoringUserIdx == 0 {
				pgaUser = "pganalyze"
			} else if monitoringUserIdx == 1 {
				err := survey.AskOne(&survey.Input{
					Message: "Enter Postgres user for the collector to use (will be saved to collector config):",
					Help:    "If the user does not exist, it can be created in a later step",
				}, &pgaUser, survey.WithValidator(survey.Required))
				if err != nil {
					return err
				}
			} else {
				panic(fmt.Sprintf("unexpected user selection: %d", monitoringUserIdx))
			}
		}

		_, err := state.CurrentSection.NewKey("db_username", pgaUser)
		if err != nil {
			return err
		}
		return state.SaveConfig()
	},
}
View Source
var SpecifyMonitoringUserPasswd = &s.Step{
	ID:          "specify_monitoring_user_password",
	Description: "Specify monitoring user password (db_password) in the collector config file",
	Check: func(state *s.SetupState) (bool, error) {
		return state.CurrentSection.HasKey("db_password"), nil
	},
	Run: func(state *s.SetupState) error {
		var passwordStrategy int
		if state.Inputs.Scripted {
			if state.Inputs.GenerateMonitoringPassword.Valid && state.Inputs.GenerateMonitoringPassword.Bool {
				if state.Inputs.Settings.DBPassword.Valid && state.Inputs.Settings.DBPassword.String != "" {
					return errors.New("cannot specify both generate password and set explicit password")
				}
				passwordStrategy = 0
			} else if state.Inputs.Settings.DBPassword.Valid && state.Inputs.Settings.DBPassword.String != "" {
				passwordStrategy = 1
			} else {
				return errors.New("no db_password specified and generate_monitoring_password flag not set")
			}
		} else {
			err := survey.AskOne(&survey.Select{
				Message: "Select how to set up the collector user password (will be saved to collector config):",
				Options: []string{"generate random password (recommended)", "enter password"},
			}, &passwordStrategy)
			if err != nil {
				return err
			}
		}

		var pgaPasswd string
		if passwordStrategy == 0 {
			passwdBytes := make([]byte, 16)
			rand.Read(passwdBytes)
			pgaPasswd = hex.EncodeToString(passwdBytes)
		} else if passwordStrategy == 1 {
			if state.Inputs.Scripted {
				pgaPasswd = state.Inputs.Settings.DBPassword.String
			} else {
				err := survey.AskOne(&survey.Input{
					Message: "Enter password for the collector to use (will be saved to collector config):",
				}, &pgaPasswd, survey.WithValidator(survey.Required))
				if err != nil {
					return err
				}
			}
		} else {
			panic(fmt.Sprintf("unexpected password option selection: %d", passwordStrategy))
		}

		_, err := state.CurrentSection.NewKey("db_password", pgaPasswd)
		if err != nil {
			return err
		}

		return state.SaveConfig()
	},
}

Functions

This section is empty.

Types

type LocalPostgres

type LocalPostgres struct {
	SocketDir string
	LocalAddr string
	Port      int
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL