├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── default.yml │ └── release.yml ├── .goreleaser.yml ├── COPYRIGHT ├── Dockerfile ├── Makefile ├── README.md ├── cmd ├── config │ ├── config.go │ └── config_test.go ├── help.go ├── pgcenter.go ├── profile │ ├── profile.go │ └── profile_test.go ├── record │ └── record.go ├── report │ ├── report.go │ └── report_test.go └── top │ └── top.go ├── config ├── config.go └── config_test.go ├── doc ├── Changelog ├── development.md ├── examples.md ├── images │ ├── pgcenter-demo.gif │ └── pgcenter-logo.png ├── pgcenter-config-readme.md ├── pgcenter-profile-readme.md ├── pgcenter-record-readme.md ├── pgcenter-report-readme.md ├── pgcenter-top-readme.md └── release-notes │ ├── v0.8.0.md │ └── v0.9.0.md ├── go.mod ├── go.sum ├── internal ├── align │ ├── align.go │ └── align_test.go ├── math │ ├── math.go │ └── math_test.go ├── postgres │ ├── connopts.go │ ├── connopts_test.go │ ├── postgres.go │ ├── postgres_test.go │ └── testing.go ├── pretty │ ├── pretty.go │ └── pretty_test.go ├── query │ ├── activity.go │ ├── activity_test.go │ ├── common.go │ ├── common_test.go │ ├── databases.go │ ├── databases_test.go │ ├── functions.go │ ├── functions_test.go │ ├── indexes.go │ ├── indexes_test.go │ ├── pgcenter_schema.go │ ├── pgcenter_schema_test.go │ ├── progress_analyze.go │ ├── progress_analyze_test.go │ ├── progress_basebackup.go │ ├── progress_basebackup_test.go │ ├── progress_cluster.go │ ├── progress_cluster_test.go │ ├── progress_copy.go │ ├── progress_copy_test.go │ ├── progress_create_index.go │ ├── progress_create_index_test.go │ ├── progress_vacuum.go │ ├── progress_vacuum_test.go │ ├── query.go │ ├── query_test.go │ ├── replication.go │ ├── replication_test.go │ ├── sizes.go │ ├── sizes_test.go │ ├── statements.go │ ├── statements_test.go │ ├── tables.go │ ├── tables_test.go │ ├── wal.go │ └── wal_test.go ├── stat │ ├── cpu.go │ ├── cpu_test.go │ ├── diskstats.go │ ├── diskstats_test.go │ ├── ethtool.go │ ├── fsstat.go │ ├── fsstat_test.go │ ├── help.go │ ├── loadavg.go │ ├── loadavg_test.go │ ├── log.go │ ├── log_test.go │ ├── memstat.go │ ├── memstat_test.go │ ├── netdev.go │ ├── netdev_test.go │ ├── postgres.go │ ├── postgres_test.go │ ├── stat.go │ ├── stat_test.go │ └── testdata │ │ ├── log │ │ └── postgresql.log │ │ ├── pgcenter.stat.golden.tar │ │ ├── pgcenter.stat.invalid.tar │ │ └── proc │ │ ├── diskstats.invalid │ │ ├── diskstats.v1.golden │ │ ├── diskstats.v2.2.golden │ │ ├── diskstats.v2.golden │ │ ├── diskstats.v3.golden │ │ ├── loadavg.golden │ │ ├── loadavg.invalid │ │ ├── meminfo.golden │ │ ├── mounts.golden │ │ ├── netdev.invalid.1 │ │ ├── netdev.invalid.2 │ │ ├── netdev.v1.golden │ │ ├── netdev.v2.golden │ │ ├── stat.golden │ │ ├── stat2.golden │ │ └── uptime.golden ├── version │ └── version.go └── view │ ├── view.go │ └── view_test.go ├── profile ├── profile.go ├── profile_test.go └── testdata │ ├── profile_header.golden │ └── profile_stats.golden ├── record ├── record.go ├── record_test.go ├── recorder.go └── recorder_test.go ├── report ├── describe.go ├── report.go ├── report_test.go └── testdata │ ├── README.md │ ├── pgcenter.stat.golden.tar │ ├── report_activity.golden │ ├── report_activity_grep.golden │ ├── report_activity_order_pid_asc.golden │ ├── report_activity_order_pid_desc.golden │ ├── report_activity_start_end.golden │ ├── report_databases_general.golden │ ├── report_databases_sessions.golden │ ├── report_entry_sample.golden │ ├── report_functions.golden │ ├── report_indexes.golden │ ├── report_progress_analyze.golden │ ├── report_progress_basebackup.golden │ ├── report_progress_cluster.golden │ ├── report_progress_copy.golden │ ├── report_progress_index.golden │ ├── report_progress_vacuum.golden │ ├── report_replication.golden │ ├── report_sample.golden │ ├── report_sizes.golden │ ├── report_statements_general.golden │ ├── report_statements_io.golden │ ├── report_statements_local.golden │ ├── report_statements_temp.golden │ ├── report_statements_timings.golden │ ├── report_statements_timings_limit.golden │ ├── report_statements_timings_limit_truncate.golden │ ├── report_statements_wal.golden │ ├── report_tables.golden │ └── report_wal.golden ├── testing ├── Dockerfile ├── README.md ├── e2e.sh ├── fixtures.sql └── prepare-test-environment.sh └── top ├── config.go ├── config_test.go ├── config_view.go ├── config_view_test.go ├── dialog.go ├── errrate.go ├── errrate_test.go ├── extra.go ├── help.go ├── keybindings.go ├── menu.go ├── menu_test.go ├── pgconfig.go ├── pglog.go ├── psql.go ├── reload.go ├── reload_test.go ├── report.go ├── report_test.go ├── reset.go ├── reset_test.go ├── signal.go ├── signal_test.go ├── stat.go ├── stat_test.go ├── top.go ├── top_test.go └── ui.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve pgCenter 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Environment** 14 | Describe the environment where the bug occurred. 15 | - OS: [output of `cat /etc/os-release`] 16 | - Docker image name and tag (in case of Docker-related environment) 17 | - pgCenter Version [output of `pgcenter --version`] 18 | - pgCenter installation method: [releases page, manual build, other?] 19 | - PostgreSQL Version [output of `psql -c 'select version()'`] 20 | - Did you try build pgCenter from master branch and reproduce the issue? ['yes' or 'no'] 21 | 22 | **To Reproduce** 23 | Describe the steps to reproduce the behavior. Attach the full error text, panic stack trace, screenshots, etc. 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Additional context** 29 | Add any other context about the problem here. This could be: 30 | - PostgreSQL error logs 31 | - Your assumptions, thoughts, hypothesis, etc 32 | - Whatever else? 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for pgCenter 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Default 3 | 4 | on: push 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | container: lesovsky/pgcenter-testing:0.0.6 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Prepare test environment 15 | run: prepare-test-environment.sh 16 | - name: Run lint 17 | run: make lint 18 | - name: Run test 19 | run: make test 20 | - name: Build 21 | run: make build 22 | - name: Install 23 | run: make install 24 | - name: Run E2E tests 25 | run: ./testing/e2e.sh 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | on: 5 | push: 6 | branches: [ release ] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | container: lesovsky/pgcenter-testing:0.0.6 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Prepare test environment 17 | run: prepare-test-environment.sh 18 | - name: Run lint 19 | run: make lint 20 | - name: Run test 21 | run: make test 22 | - name: Build 23 | run: make build 24 | - name: Install 25 | run: make install 26 | - name: Run E2E tests 27 | run: ./testing/e2e.sh 28 | 29 | build: 30 | runs-on: ubuntu-latest 31 | needs: test 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v2 35 | with: 36 | fetch-depth: 0 37 | - name: Build image 38 | run: make docker-build 39 | - name: Log in to Docker Hub 40 | run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} 41 | - name: Push image to Docker Hub 42 | run: make docker-push 43 | 44 | goreleaser: 45 | runs-on: ubuntu-latest 46 | needs: [ test, build ] 47 | steps: 48 | - uses: actions/checkout@v2 49 | with: 50 | fetch-depth: 0 51 | - uses: actions/setup-go@v2 52 | with: 53 | go-version: 1.16 54 | - uses: goreleaser/goreleaser-action@v2 55 | with: 56 | version: latest 57 | args: release --rm-dist 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - make dep 4 | 5 | builds: 6 | - binary: pgcenter 7 | main: ./cmd 8 | goarch: 9 | - amd64 10 | goos: 11 | - linux 12 | env: 13 | - CGO_ENABLED=0 14 | ldflags: 15 | - -a -installsuffix cgo 16 | - -X github.com/lesovsky/pgcenter/internal/version.gitTag={{.Tag}} 17 | - -X github.com/lesovsky/pgcenter/internal/version.gitCommit={{.Commit}} 18 | - -X github.com/lesovsky/pgcenter/internal/version.gitBranch={{.Branch}} 19 | 20 | archives: 21 | - builds: [pgcenter] 22 | 23 | nfpms: 24 | - vendor: pgcenter 25 | homepage: https://github.com/lesovsky/pgcenter 26 | maintainer: Alexey Lesovsky 27 | description: Command-line admin tool for observing and troubleshooting Postgres. 28 | license: BSD-3 29 | formats: [ deb, rpm, apk ] 30 | bindir: /usr/bin -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | (C) 2017 by Alexey Lesovsky (lesovsky gmail.com) 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | - Redistributions of source code must retain the above copyright notice, 6 | this list of conditions and the following disclaimer. 7 | - Redistributions in binary form must reproduce the above copyright notice, 8 | this list of conditions and the following disclaimer in the documentation and/or 9 | other materials provided with the distribution. 10 | - Neither the name Alexey Lesovsky nor the names of other contributors may be 11 | used to endorse or promote products derived from this software without specific 12 | prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE Alexey Lesovsky AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND Alexey Lesovsky OR 18 | CONTRIBUTORS HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 19 | ENHANCEMENTS, OR MODIFICATIONS. 20 | 21 | IN NO EVENT SHALL THE Alexey Lesovsky OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 22 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 23 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 27 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # __release_tag__ golang 1.16 was released 2021-02-16 2 | # __release_tag__ alpine 3.13 was released 2021-01-14 3 | 4 | # stage 1: build 5 | FROM golang:1.16 as build 6 | LABEL stage=intermediate 7 | WORKDIR /app 8 | COPY . . 9 | RUN make build 10 | 11 | # stage 2: scratch 12 | FROM alpine:3.13 as scratch 13 | COPY --from=build /app/bin/pgcenter /bin/pgcenter 14 | CMD ["pgcenter"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_ACCOUNT = lesovsky 2 | PROGRAM_NAME = pgcenter 3 | 4 | PKG_PATH = github.com/lesovsky/pgcenter 5 | COMMIT=$(shell git rev-parse --short HEAD) 6 | BRANCH=$(shell git rev-parse --abbrev-ref HEAD) 7 | TAG=$(shell git describe --tags |cut -d- -f1) 8 | 9 | LDFLAGS = -ldflags "-X ${PKG_PATH}/internal/version.gitTag=${TAG} -X ${PKG_PATH}/internal/version.gitCommit=${COMMIT} -X ${PKG_PATH}/internal/version.gitBranch=${BRANCH}" 10 | 11 | .PHONY: help clean dep build install uninstall 12 | 13 | .DEFAULT_GOAL := help 14 | 15 | help: ## Display this help screen. 16 | @echo "Makefile available targets:" 17 | @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " * \033[36m%-15s\033[0m %s\n", $$1, $$2}' 18 | 19 | clean: ## Clean build directory. 20 | rm -f ./bin/${PROGRAM_NAME} 21 | rmdir ./bin 22 | 23 | dep: ## Download the dependencies. 24 | go mod download 25 | 26 | lint: dep ## Lint the source files 27 | golangci-lint run --timeout 5m -E golint -e '(struct field|type|method|func) [a-zA-Z`]+ should be [a-zA-Z`]+' 28 | gosec -quiet ./... 29 | 30 | test: dep ## Run tests 31 | go test -race -p 1 -timeout 300s -coverprofile=.test_coverage.txt ./... && \ 32 | go tool cover -func=.test_coverage.txt | tail -n1 | awk '{print "Total test coverage: " $$3}' 33 | @rm .test_coverage.txt 34 | 35 | build: dep ## Build pgcenter executable. 36 | mkdir -p ./bin 37 | CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} go build ${LDFLAGS} -o bin/${PROGRAM_NAME} ./cmd 38 | 39 | install: ## Install pgcenter executable into /usr/bin directory. 40 | install -pm 755 bin/${PROGRAM_NAME} /usr/bin/${PROGRAM_NAME} 41 | 42 | uninstall: ## Uninstall pgcenter executable from /usr/bin directory. 43 | rm -f /usr/bin/${PROGRAM_NAME} 44 | 45 | docker-build: ## Build docker image 46 | docker build -t ${DOCKER_ACCOUNT}/${PROGRAM_NAME}:${TAG} . 47 | docker tag ${DOCKER_ACCOUNT}/${PROGRAM_NAME}:${TAG} ${DOCKER_ACCOUNT}/${PROGRAM_NAME}:latest 48 | docker image prune --force --filter label=stage=intermediate 49 | 50 | docker-push: ## Push docker image to registry 51 | docker push ${DOCKER_ACCOUNT}/${PROGRAM_NAME}:${TAG} 52 | docker push ${DOCKER_ACCOUNT}/${PROGRAM_NAME}:latest -------------------------------------------------------------------------------- /cmd/config/config.go: -------------------------------------------------------------------------------- 1 | // Entry point for 'pgcenter config' command. 2 | 3 | package config 4 | 5 | import ( 6 | "fmt" 7 | "github.com/lesovsky/pgcenter/config" 8 | "github.com/lesovsky/pgcenter/internal/postgres" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | localOptions options 14 | connOptions postgres.ConnectionOptions 15 | 16 | // CommandDefinition defines 'config' sub-command. 17 | CommandDefinition = &cobra.Command{ 18 | Use: "config", 19 | Short: "installs or uninstalls pgcenter stats schema to Postgres", 20 | Long: `'pgcenter config' installs or uninstalls pgcenter stats schema to Postgres.`, 21 | RunE: func(command *cobra.Command, args []string) error { 22 | // Parse extra arguments. 23 | if len(args) > 0 { 24 | connOptions.ParseExtraArgs(args) 25 | } 26 | 27 | // Create connection config. 28 | pgConfig, err := postgres.NewConfig(connOptions.Host, connOptions.Port, connOptions.User, connOptions.Dbname) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | // Validate local options. 34 | err = localOptions.validate() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | // Select runtime mode. 40 | mode := localOptions.mode() 41 | 42 | return config.RunMain(pgConfig, mode) 43 | }, 44 | } 45 | ) 46 | 47 | func init() { 48 | CommandDefinition.Flags().StringVarP(&connOptions.Host, "host", "h", "", "database server host or socket directory") 49 | CommandDefinition.Flags().IntVarP(&connOptions.Port, "port", "p", 5432, "database server port") 50 | CommandDefinition.Flags().StringVarP(&connOptions.User, "username", "U", "", "database user name") 51 | CommandDefinition.Flags().StringVarP(&connOptions.Dbname, "dbname", "d", "", "database name to connect to") 52 | CommandDefinition.Flags().BoolVarP(&localOptions.install, "install", "i", false, "install stats schema into the database") 53 | CommandDefinition.Flags().BoolVarP(&localOptions.uninstall, "uninstall", "u", false, "uninstall stats schema from the database") 54 | } 55 | 56 | // options defines set of options used only in 'pgcenter config' scope 57 | type options struct { 58 | install bool 59 | uninstall bool 60 | } 61 | 62 | // validate performs sanity checks of passed options 63 | func (opts *options) validate() error { 64 | if !opts.install && !opts.uninstall { 65 | return fmt.Errorf("using '--install' or '--uninstall' options are mandatory") 66 | } 67 | 68 | if opts.install == opts.uninstall { 69 | return fmt.Errorf("can't use '--install' and '--uninstall' options together") 70 | } 71 | 72 | return nil 73 | } 74 | 75 | // mode return runtime mode 76 | func (opts *options) mode() int { 77 | if opts.install { 78 | return config.Install 79 | } 80 | if opts.uninstall { 81 | return config.Uninstall 82 | } 83 | return -1 84 | } 85 | -------------------------------------------------------------------------------- /cmd/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/config" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_options_validate(t *testing.T) { 10 | testcases := []struct { 11 | in options 12 | valid bool 13 | }{ 14 | {in: options{install: true, uninstall: false}, valid: true}, 15 | {in: options{install: false, uninstall: true}, valid: true}, 16 | {in: options{install: false, uninstall: false}, valid: false}, 17 | {in: options{install: true, uninstall: true}, valid: false}, 18 | } 19 | 20 | for _, tc := range testcases { 21 | got := tc.in.validate() 22 | if tc.valid { 23 | assert.NoError(t, got) 24 | } else { 25 | assert.Error(t, got) 26 | } 27 | } 28 | } 29 | 30 | func Test_options_mode(t *testing.T) { 31 | testcases := []struct { 32 | in options 33 | want int 34 | }{ 35 | {in: options{install: true}, want: config.Install}, 36 | {in: options{uninstall: true}, want: config.Uninstall}, 37 | {in: options{}, want: -1}, 38 | } 39 | 40 | for _, tc := range testcases { 41 | assert.Equal(t, tc.want, tc.in.mode()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cmd/pgcenter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/cmd/config" 6 | "github.com/lesovsky/pgcenter/cmd/profile" 7 | "github.com/lesovsky/pgcenter/cmd/record" 8 | "github.com/lesovsky/pgcenter/cmd/report" 9 | "github.com/lesovsky/pgcenter/cmd/top" 10 | "github.com/lesovsky/pgcenter/internal/version" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // pgcenter describes the root command of program 15 | var pgcenter = &cobra.Command{ 16 | Short: "Admin tool for PostgreSQL", 17 | Long: "pgCenter is a command line admin tool for PostgreSQL.", 18 | SilenceUsage: true, 19 | SilenceErrors: true, 20 | } 21 | 22 | func init() { 23 | pgcenter.PersistentFlags().BoolP("help", "?", false, "show this help and exit") 24 | 25 | name, tag, commit, branch := version.Version() 26 | versionStr := fmt.Sprintf("%s %s %s-%s\n", name, tag, commit, branch) 27 | 28 | pgcenter.Use = name 29 | pgcenter.Version = versionStr 30 | 31 | // Setup help and versions templates for main program 32 | pgcenter.SetVersionTemplate(versionStr) 33 | pgcenter.SetHelpTemplate(printMainHelp()) 34 | 35 | // Setup 'config' sub-command 36 | pgcenter.AddCommand(config.CommandDefinition) 37 | config.CommandDefinition.SetVersionTemplate(versionStr) 38 | config.CommandDefinition.SetHelpTemplate(printConfigHelp()) 39 | config.CommandDefinition.SetUsageTemplate(printConfigHelp()) 40 | 41 | // Setup 'profile' sub-command 42 | pgcenter.AddCommand(profile.CommandDefinition) 43 | profile.CommandDefinition.SetVersionTemplate(versionStr) 44 | profile.CommandDefinition.SetHelpTemplate(printProfileHelp()) 45 | profile.CommandDefinition.SetUsageTemplate(printProfileHelp()) 46 | 47 | // Setup 'record' sub-command 48 | pgcenter.AddCommand(record.CommandDefinition) 49 | record.CommandDefinition.SetVersionTemplate(versionStr) 50 | record.CommandDefinition.SetHelpTemplate(printRecordHelp()) 51 | record.CommandDefinition.SetUsageTemplate(printRecordHelp()) 52 | 53 | // Setup 'report' sub-command 54 | pgcenter.AddCommand(report.CommandDefinition) 55 | report.CommandDefinition.SetVersionTemplate(versionStr) 56 | report.CommandDefinition.SetHelpTemplate(printReportHelp()) 57 | report.CommandDefinition.SetUsageTemplate(printReportHelp()) 58 | 59 | // Setup 'top' sub-command 60 | pgcenter.AddCommand(top.CommandDefinition) 61 | top.CommandDefinition.SetVersionTemplate(versionStr) 62 | top.CommandDefinition.SetHelpTemplate(printTopHelp()) 63 | top.CommandDefinition.SetUsageTemplate(printTopHelp()) 64 | } 65 | 66 | func main() { 67 | if err := pgcenter.Execute(); err != nil { 68 | fmt.Println(err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cmd/profile/profile.go: -------------------------------------------------------------------------------- 1 | // Entry point for 'pgcenter profile' command. 2 | 3 | package profile 4 | 5 | import ( 6 | "fmt" 7 | "github.com/lesovsky/pgcenter/internal/postgres" 8 | "github.com/lesovsky/pgcenter/profile" 9 | "github.com/spf13/cobra" 10 | "time" 11 | ) 12 | 13 | var ( 14 | profileConfig profile.Config 15 | connOptions postgres.ConnectionOptions 16 | 17 | // CommandDefinition is the definition of 'profile' CLI sub-command 18 | CommandDefinition = &cobra.Command{ 19 | Use: "profile", 20 | Short: "wait events profiler", 21 | Long: `'pgcenter profile' profiles wait events of running queries.`, 22 | RunE: func(command *cobra.Command, args []string) error { 23 | // Parse extra arguments. 24 | if len(args) > 0 { 25 | connOptions.ParseExtraArgs(args) 26 | } 27 | 28 | // Create connection config. 29 | pgConfig, err := postgres.NewConfig(connOptions.Host, connOptions.Port, connOptions.User, connOptions.Dbname) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | err = validate(profileConfig) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return profile.RunMain(pgConfig, profileConfig) 40 | }, 41 | } 42 | ) 43 | 44 | func init() { 45 | CommandDefinition.Flags().StringVarP(&connOptions.Host, "host", "h", "", "database server host or socket directory") 46 | CommandDefinition.Flags().IntVarP(&connOptions.Port, "port", "p", 5432, "database server port") 47 | CommandDefinition.Flags().StringVarP(&connOptions.User, "username", "U", "", "database user name") 48 | CommandDefinition.Flags().StringVarP(&connOptions.Dbname, "dbname", "d", "", "database name to connect to") 49 | CommandDefinition.Flags().IntVarP(&profileConfig.Pid, "pid", "P", 0, "PID of Postgres backend to profile to") 50 | CommandDefinition.Flags().DurationVarP(&profileConfig.Frequency, "freq", "F", 100*time.Millisecond, "profile with this frequency (default: 100ms)") 51 | CommandDefinition.Flags().IntVarP(&profileConfig.Strsize, "strsize", "s", 128, "limit length of print query strings to STRSIZE chars (default: 128)") 52 | CommandDefinition.Flags().BoolVarP(&profileConfig.NoWorkers, "no-workers", "W", false, "don't profile child parallel workers (default: false)") 53 | 54 | _ = CommandDefinition.MarkFlagRequired("pid") 55 | } 56 | 57 | func validate(config profile.Config) error { 58 | if config.Frequency < time.Millisecond || config.Frequency > time.Second { 59 | return fmt.Errorf("invalid profile frequency, must be between 1 millisecond and 1 second") 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /cmd/profile/profile_test.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/profile" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func Test_validate(t *testing.T) { 11 | testcases := []struct { 12 | valid bool 13 | cfg profile.Config 14 | }{ 15 | {valid: true, cfg: profile.Config{Frequency: 50 * time.Millisecond}}, 16 | {valid: false, cfg: profile.Config{Frequency: time.Millisecond - 1}}, 17 | {valid: false, cfg: profile.Config{Frequency: time.Second + 1}}, 18 | } 19 | 20 | for _, tc := range testcases { 21 | err := validate(tc.cfg) 22 | if tc.valid { 23 | assert.NoError(t, err) 24 | } else { 25 | assert.Error(t, err) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cmd/record/record.go: -------------------------------------------------------------------------------- 1 | // Entry point for 'pgcenter record' command. 2 | 3 | package record 4 | 5 | import ( 6 | "github.com/lesovsky/pgcenter/internal/postgres" 7 | "github.com/lesovsky/pgcenter/record" 8 | "github.com/spf13/cobra" 9 | "time" 10 | ) 11 | 12 | var ( 13 | recordConfig record.Config 14 | connOptions postgres.ConnectionOptions 15 | oneshot bool 16 | 17 | // CommandDefinition defines 'record' sub-command. 18 | CommandDefinition = &cobra.Command{ 19 | Use: "record", 20 | Short: "record stats to file", 21 | Long: `'pgcenter record' connects to PostgreSQL and collects stats into local file.`, 22 | RunE: func(command *cobra.Command, args []string) error { 23 | // Convert 'oneshot' to set of options. 24 | if oneshot { 25 | recordConfig.AppendFile = true 26 | recordConfig.Count = 1 27 | recordConfig.Interval = time.Millisecond // interval must not be zero - ticker will panic. 28 | } 29 | 30 | // Parse extra arguments. 31 | if len(args) > 0 { 32 | connOptions.ParseExtraArgs(args) 33 | } 34 | 35 | // Create connection config. 36 | pgConfig, err := postgres.NewConfig(connOptions.Host, connOptions.Port, connOptions.User, connOptions.Dbname) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return record.RunMain(pgConfig, recordConfig) 42 | }, 43 | } 44 | ) 45 | 46 | func init() { 47 | defaultRecordFile := "pgcenter.stat.tar" 48 | 49 | CommandDefinition.Flags().StringVarP(&connOptions.Host, "host", "h", "", "database server host or socket directory") 50 | CommandDefinition.Flags().IntVarP(&connOptions.Port, "port", "p", 5432, "database server port") 51 | CommandDefinition.Flags().StringVarP(&connOptions.User, "username", "U", "", "database user name") 52 | CommandDefinition.Flags().StringVarP(&connOptions.Dbname, "dbname", "d", "", "database name to connect to") 53 | CommandDefinition.Flags().DurationVarP(&recordConfig.Interval, "interval", "i", time.Second, "statistics recording interval (default: 1 second)") 54 | CommandDefinition.Flags().IntVarP(&recordConfig.Count, "count", "c", -1, "number of statistics samples to record") 55 | CommandDefinition.Flags().StringVarP(&recordConfig.OutputFile, "file", "f", defaultRecordFile, "file where statistics are saved") 56 | CommandDefinition.Flags().BoolVarP(&recordConfig.AppendFile, "append", "a", false, "append statistics to file (default: true)") 57 | CommandDefinition.Flags().IntVarP(&recordConfig.StringLimit, "strlimit", "t", 0, "maximum query length to record (default: 0, no limit)") 58 | CommandDefinition.Flags().BoolVarP(&oneshot, "oneshot", "1", false, "append single statistics snapshot to file and exit") 59 | } 60 | -------------------------------------------------------------------------------- /cmd/top/top.go: -------------------------------------------------------------------------------- 1 | // Entry point for 'pgcenter top' command. 2 | 3 | package top 4 | 5 | import ( 6 | "github.com/lesovsky/pgcenter/internal/postgres" 7 | "github.com/lesovsky/pgcenter/top" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | opts postgres.ConnectionOptions 13 | 14 | // CommandDefinition defines 'top' sub-command. 15 | CommandDefinition = &cobra.Command{ 16 | Use: "top", 17 | Short: "top-like stats viewer", 18 | Long: `'pgcenter top' is the top-like stats viewer.`, 19 | RunE: func(command *cobra.Command, args []string) error { 20 | // Parse extra arguments. 21 | if len(args) > 0 { 22 | opts.ParseExtraArgs(args) 23 | } 24 | 25 | // Create connection config. 26 | pgConfig, err := postgres.NewConfig(opts.Host, opts.Port, opts.User, opts.Dbname) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return top.RunMain(pgConfig) 32 | }, 33 | } 34 | ) 35 | 36 | // Parse user passed parameters values and arguments. 37 | func init() { 38 | CommandDefinition.Flags().StringVarP(&opts.Host, "host", "h", "", "database server host or socket directory") 39 | CommandDefinition.Flags().IntVarP(&opts.Port, "port", "p", 0, "database server port") 40 | CommandDefinition.Flags().StringVarP(&opts.User, "username", "U", "", "database user name") 41 | CommandDefinition.Flags().StringVarP(&opts.Dbname, "dbname", "d", "", "database name to connect to") 42 | } 43 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/lesovsky/pgcenter/internal/postgres" 7 | "github.com/lesovsky/pgcenter/internal/query" 8 | ) 9 | 10 | const ( 11 | // Install flag tells to install schema. 12 | Install = iota 13 | // Uninstall flag tells to uninstall schema. 14 | Uninstall 15 | ) 16 | 17 | // RunMain is the main entry point for 'pgcenter config' command. 18 | func RunMain(dbConfig postgres.Config, mode int) error { 19 | db, err := postgres.Connect(dbConfig) 20 | if err != nil { 21 | return err 22 | } 23 | defer db.Close() 24 | 25 | switch mode { 26 | case Install: 27 | if err := doInstall(db); err != nil { 28 | return err 29 | } 30 | fmt.Printf("pgCenter schema installed.") 31 | case Uninstall: 32 | if err := doUninstall(db); err != nil { 33 | return err 34 | } 35 | fmt.Printf("pgCenter schema uninstalled.") 36 | default: 37 | // should not be here, but who knows... 38 | fmt.Printf("do nothing, unknown mode selected.") 39 | return fmt.Errorf("unknown mode selected") 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // doInstall begins transaction and create pgcenter schema, functions and views. 46 | func doInstall(db *postgres.DB) error { 47 | queries := []string{ 48 | query.StatSchemaCreateSchema, 49 | query.StatSchemaCreateFunction1, 50 | query.StatSchemaCreateFunction2, 51 | query.StatSchemaCreateFunction3, 52 | query.StatSchemaCreateFunction4, 53 | query.StatSchemaCreateView1, 54 | query.StatSchemaCreateView2, 55 | query.StatSchemaCreateView3, 56 | query.StatSchemaCreateView4, 57 | query.StatSchemaCreateView5, 58 | query.StatSchemaCreateView6, 59 | query.StatSchemaCreateView7, 60 | } 61 | 62 | tx, err := db.Conn.Begin(context.Background()) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | for _, q := range queries { 68 | _, err := tx.Exec(context.Background(), q) 69 | if err != nil { 70 | _ = tx.Rollback(context.Background()) 71 | return err 72 | } 73 | } 74 | 75 | err = tx.Commit(context.Background()) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // doUninstall drops pgcenter stats schema. 84 | func doUninstall(db *postgres.DB) error { 85 | _, err := db.Exec(query.StatSchemaDropSchema) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestRunMain(t *testing.T) { 10 | config, err := postgres.NewTestConfig() 11 | assert.NoError(t, err) 12 | 13 | // run tests in dedicated database to avoid interfering with other test which depends on stats schema 14 | config.Config.Database = config.Config.Database + "_config" 15 | 16 | assert.NoError(t, RunMain(config, Install)) 17 | assert.NoError(t, RunMain(config, Uninstall)) 18 | } 19 | -------------------------------------------------------------------------------- /doc/development.md: -------------------------------------------------------------------------------- 1 | ### Development 2 | 3 | Download pgcenter-testing docker image, run container and start script which prepares testing environment. 4 | ```shell 5 | $ docker pull lesovsky/pgcenter-testing:latest 6 | $ docker run --rm -p 21995:21995 -p 21996:21996 -p 21910:21910 -p 21911:21911 -p 21912:21912 -p 21913:21913 -p 21914:21914 -ti lesovsky/pgcenter-testing:latest /bin/bash 7 | # prepare-test-environment.sh 8 | ``` 9 | 10 | Clone the repo. 11 | ```shell 12 | $ git clone https://github.com/lesovsky/pgcenter 13 | $ cd pgcenter 14 | ``` 15 | 16 | Before running tests or building make sure your go version is 1.16. 17 | 18 | Run tests. 19 | ```shell 20 | $ make lint 21 | $ make test 22 | ``` 23 | 24 | Build. 25 | ```shell 26 | $ make build 27 | ``` 28 | 29 | ### Names convention 30 | All statistics fields have to follow the naming convention. 31 | 1. Add '_total' suffix to **cumulative** integer/float values: 32 | - `calls_total 12345` - Total number of calls. 33 | - `backends_total 123` - Current number of connected backends. 34 | 35 | 36 | 2. Add '_total' or '_age' (depends on value context) suffix to **cumulative** time values in human-readable format: 37 | - `exec_total 03:30:41` - Total time spent executing queries. 38 | - `stats_age 04:15:33` - Age of stats since last reset. 39 | 40 | 41 | 3. Don't use suffixes, for **rate** values: 42 | - `commits 123` - Number of commits per second. 43 | - `inserts 456` - Number of inserts per second. 44 | 45 | 46 | 4. Add unit for values measured in bytes, seconds or ratios: 47 | - `read,ms 10.00` - Number of milliseconds spent reading table. 48 | - `size_total,KiB 45.00` - Total size of something. 49 | - `processed,% 85.05` - Current ratio of something processed. 50 | 51 | 52 | 5. Use free form for fields with text values: 53 | - `database pgbench` - Name of database. 54 | -------------------------------------------------------------------------------- /doc/examples.md: -------------------------------------------------------------------------------- 1 | ### README: Examples 2 | 3 | - [General notes](#general-notes) 4 | - [Download](#download) 5 | - [Run in Docker](#run-in-docker) 6 | - [pgCenter usage examples](#pgcenter-usage) 7 | --- 8 | 9 | #### General notes 10 | - recommended running pgCenter on the same host with Postgres, otherwise some features will not work, e.g. config editing, logfile view. 11 | - run pgCenter using database `SUPERUSER` account, e.g. postgres. Some kind of stats aren't available for unprivileged accounts. 12 | - Connections established to Postgres are managed by [jackc/pgx](https://github.com/jackc/pgx/) driver which supports [.pgpass](https://www.postgresql.org/docs/current/static/libpq-pgpass.html) and most of common libpq [environment variables](https://www.postgresql.org/docs/current/static/libpq-envars.html), such as PGHOST, PGPORT, PGUSER, PGDATABASE, PGPASSWORD, PGOPTIONS. 13 | 14 | #### Download 15 | Download the latest release from [release page](https://github.com/lesovsky/pgcenter/releases), install using package manager or unpack from `.tar.gz` archive. Now, pgCenter is ready to run. 16 | 17 | #### Run in Docker 18 | There is option to run pgCenter using Docker. Docker images available on [DockerHub](https://hub.docker.com/r/lesovsky/pgcenter). 19 | ``` 20 | docker pull lesovsky/pgcenter:latest 21 | docker run -it --rm lesovsky/pgcenter:latest pgcenter top -h 1.2.3.4 -U user -d production_db 22 | ``` 23 | 24 | #### pgCenter usage 25 | pgCenter's functionality is splitted among several sub-commands, run specific one to achieve your goals. 26 | In most cases, connection setting can be omitted. 27 | 28 | - Run `top` command to connect to Postgres and watching statistics: 29 | ``` 30 | pgcenter top -h 1.2.3.4 -U postgres production_db 31 | ``` 32 | 33 | - Run `profile` command to connect to Postgres and profile backend with PID 12345: 34 | ``` 35 | pgcenter profile -U postgres -P 12345 production_db 36 | ``` 37 | 38 | - Run `profile` command to profile backend with PID 12345 with frequency 50 (every 20ms): 39 | ``` 40 | pgcenter profile -U postgres -P 12345 -F 50 production_db 41 | ``` 42 | 43 | - Run `record` command to connect to Postgres, poll statistics and continuously save to a local file: 44 | ``` 45 | pgcenter record -f /tmp/stats.tar -U postgres production_db 46 | ``` 47 | 48 | - Run `report` command to read the previously written file and build a report: 49 | ``` 50 | pgcenter report -f /tmp/stats.tar --databases 51 | ``` 52 | 53 | - Run `report` command, build activity report with start time 12:30:00 and end time 12:50:00: 54 | ``` 55 | pgcenter report --activity --start 12:30:00 --end 12:50:00 56 | ``` 57 | 58 | - Run `report` command, build tables report order by `seq_scan` column and show only 2 tables per single stat snapshot: 59 | ``` 60 | pgcenter report --tables --order seq_scan --limit 2 61 | ``` 62 | - Run `report` command, build statements report and show statements that have `UPDATE` word in `query` column: 63 | ``` 64 | pgcenter report --statements m --grep query:UPDATE 65 | ``` 66 | 67 | Full list of available parameters available in a built-in help for particular command, use `--help` parameter. 68 | 69 | ``` 70 | pgcenter report --help 71 | ``` 72 | -------------------------------------------------------------------------------- /doc/images/pgcenter-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lesovsky/pgcenter/c4d5111cdca7f2ab6ddd4367620ea1f32cc6123b/doc/images/pgcenter-demo.gif -------------------------------------------------------------------------------- /doc/images/pgcenter-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lesovsky/pgcenter/c4d5111cdca7f2ab6ddd4367620ea1f32cc6123b/doc/images/pgcenter-logo.png -------------------------------------------------------------------------------- /doc/pgcenter-profile-readme.md: -------------------------------------------------------------------------------- 1 | ### README: pgcenter profile 2 | 3 | `pgcenter profile` is the tool for profiling [wait events](https://www.postgresql.org/docs/current/monitoring-stats.html#WAIT-EVENT-TABLE) occured during queries execution. 4 | 5 | - [General information](#general-information) 6 | - [Main functions](#main-functions) 7 | - [Limitations](#limitations) 8 | - [Usage](#usage) 9 | --- 10 | 11 | #### General information 12 | In cases of long query, you might be interested what this query does. Using `EXPLAIN` utility you can observe detailed query execution plan. But if query spends time in waitings `EXPLAIN` will not show that. Using `pgcenter profile` you can see what wait events occur during query execution. Below an example of wait events occured in heavy `UPDATE` query on system with poor IO performance: 13 | ``` 14 | ------ ------------ ----------------------------- 15 | % time seconds wait_event query: update pgbench_accounts set abalance = abalance + 100; 16 | ------ ------------ ----------------------------- 17 | 72.15 30.205671 IO.DataFileRead 18 | 20.10 8.415921 Running 19 | 5.50 2.303926 LWLock.WALWriteLock 20 | 1.28 0.535915 IO.DataFileWrite 21 | 0.54 0.225117 IO.WALWrite 22 | 0.36 0.152407 IO.WALInitSync 23 | 0.03 0.011429 IO.WALInitWrite 24 | 0.03 0.011355 LWLock.WALBufMappingLock 25 | ------ ------------ ----------------------------- 26 | 99.99 41.861741 27 | ``` 28 | In the example, you can see that a most of time is spent on awaiting IO when reading data files. 29 | 30 | Exploring your queries with `pgcenter profiler` you can see many other interesting things. 31 | 32 | #### Main functions 33 | - using `pid`, `wait_event_type`, `wait_event` from `pg_stat_activity` statistics for profiling; 34 | - specify the PID for profiling a specific Postgres backend; 35 | - change the frequency of profiling interval; default is 100, means to profile with 10ms interval. 36 | 37 | #### Limitations 38 | - [Wait events](https://www.postgresql.org/docs/current/monitoring-stats.html#WAIT-EVENT-TABLE) has been introduced in Postgres 9.6, hence the profiling is possible for 9.6 and newer versions of Postgres. 39 | - Profiling wait events for parallel workers is working only since Postgres 13, because there is no guaranteed way to associate master process with its workers in earlier versions. 40 | 41 | #### Usage 42 | Run `profile` and specify backend PID which want to profile to: 43 | ``` 44 | pgcenter profile -U postgres -P 12345 45 | ``` 46 | 47 | See other usage examples [here](examples.md). -------------------------------------------------------------------------------- /doc/pgcenter-record-readme.md: -------------------------------------------------------------------------------- 1 | ### README: pgcenter record 2 | 3 | `pgcenter record` is the first tool from "Poor man's monitoring" which collect metrics from Postgres to local files. 4 | 5 | - [General information](#general-information) 6 | - [Main functions](#main-functions) 7 | - [Usage](#usage) 8 | --- 9 | 10 | #### General information 11 | `pgcenter record` can be used in cases when no monitoring is available, but there is a need to collect Postgres performance statistics over time. It can also be used as an ad-hoc statistics collecting tool when there is a need in gathering statistics over short period of time for purposes of later analysis, for example collecting stats at benchmarking. 12 | 13 | `pgcenter record` connects to Postgres, reads stats and writes this information into JSON files into a tar archive. File names contain name of statistics view and timestamp when stats have been recorded. Hence, it's possible to unpack statistics using `tar`. Once unpacked, stats can be used in any way required. 14 | 15 | For reading and building of various different reports there is an alternative tool: `pgcenter report`. See details [here](pgcenter-report-readme.md). 16 | 17 | #### Main functions 18 | - continuous recording of statistics into JSON files packed into tar file; 19 | - recording of statistics with specified interval or specified number of times; 20 | - oneshot mode - record single snapshot of statistics and append it into an existing file. 21 | 22 | `pgcenter record` doesn't support recording of system statistics, but if you are interested in  such tool, take a look at `sar` utility from `sysstat` package. 23 | 24 | #### Usage 25 | Run `record` command to connect to Postgres, poll statistics and continuously save to a local file: 26 | ``` 27 | pgcenter record -f /tmp/stats.tar -U postgres production_db 28 | ``` 29 | 30 | See other usage examples [here](examples.md). -------------------------------------------------------------------------------- /doc/pgcenter-report-readme.md: -------------------------------------------------------------------------------- 1 | ### README: pgcenter report 2 | 3 | `pgcenter report` is the second part of "Poor man's monitoring" which reads stats files written by pgcenter record and build reports. 4 | 5 | - [General information](#general-information) 6 | - [Main functions](#main-functions) 7 | - [Usage](#usage) 8 | --- 9 | 10 | #### General information 11 | `pgcenter report` is an addition to pgcenter record. It reads  the collected statistics  and builds reports out of these data. 12 | 13 | `pgcenter report` doesn't require connection to Postgres, all you need  is to specify the file with relevant statistics and choose the type of the report. 14 | 15 | #### Main functions 16 | - building reports from wide spectrum of Postgres stats; 17 | - building reports based on start and end times; 18 | - specifying sort order based on values of specified column; 19 | - filtering stats to show only relevant information (support regular expressions); 20 | - limiting the amount of printed stats and showing only required information; 21 | - showing short description of stats columns - no need to visit Postgres documentation (limited feature, will be expanded in next releases). 22 | 23 | #### Usage 24 | Run `report` command to read previously written file and build a report about databases: 25 | ``` 26 | pgcenter report -f /tmp/stats.tar --database 27 | ``` 28 | 29 | See other usage examples [here](examples.md). -------------------------------------------------------------------------------- /doc/pgcenter-top-readme.md: -------------------------------------------------------------------------------- 1 | ### README: pgcenter top 2 | 3 | `pgcenter top` provides top-like interface for Postgres statistics with extended set of functions that make online monitoring and troubleshooting Postgres much easier. 4 | 5 | - [General information](#general-information) 6 | - [Main functions](#main-functions) 7 | - [Admin functions](#admin-functions) 8 | - [System statistics notes](#system-statistics-notes) 9 | - [Usage](#usage) 10 | --- 11 | 12 | #### General information 13 | `pgcenter top` relies on two types of statistics: 14 | 15 | 1. statistics about system resources usage from `procfs` filesystem; 16 | 2. activity statistics from Postgres. 17 | 18 | At launch, pgCenter connects to Postgres and starts continuously reading statistics views. Comparing stats snapshots pgCenter calculates differences and shows it to user. Same goes for system stats, pgCenter reads stats files from `/proc` filesystem, calculates differences and shows results to user. 19 | 20 | #### Main functions 21 | It may be surprising, but Postgres can provide hundreds and thousands of stats metrics distributed across several functions and views. `pgcenter top` helps not to drown in statistics with: 22 | - console-based top-like interface; 23 | - keyboard shortcuts to switch between different kind of stats; 24 | - ascending and descending sort order based on values from particular columns; 25 | - ability to filter unnecessary statistics and only focus on relevant data. 26 | 27 | #### Admin functions: 28 | `pgcenter top` also provides admin functions that assist in Postgres administration and troubleshooting. It allows user to: 29 | - view current configuration, edit configuration files and reload Postgres service; 30 | - view log files in pager or view log's tail on the fly; 31 | - cancel queries or terminate backends using backend's pid; 32 | - cancel group of queries or terminate group of backends based on their states; 33 | - toggle displaying system tables and indexes for tables and indexes statistics; 34 | - reset Postgres statistics counters; 35 | - view detailed reports about statements (based on `pg_stat_statements`); 36 | - start `psql` session (if you prefer a hands-on approach). 37 | 38 | Note, though admin functions allows managing Postgres configuration, pgCenter is not a comprehensive tool for Postgres configurations and services management. 39 | 40 | #### System statistics notes 41 | - system statistics are available through `procfs` filesystem which is available on Linux operating system. It is not available on other operating systems, e.g. Windows. 42 | 43 | Though, `procfs` is  available in other UNIX systems, its variants may differ from Linux `procfs` so may not be supported by pgCenter. 44 | 45 | - `pgcenter top` can connect to remote Postgres services and retrieve system statistics through additional SQL functions that are shipped with pgCenter. See details [here](). 46 | 47 | #### Usage 48 | Run `top` command to connect to Postgres and watching statistics: 49 | ``` 50 | pgcenter top -h 1.2.3.4 -U postgres production_db 51 | ``` 52 | 53 | See other usage examples [here](examples.md). -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lesovsky/pgcenter 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | github.com/jackc/pgconn v1.14.3 7 | github.com/jackc/pgx/v4 v4.18.3 8 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 9 | github.com/jroimartin/gocui v0.5.0 10 | github.com/spf13/cobra v1.8.0 11 | github.com/stretchr/testify v1.9.0 12 | golang.org/x/crypto v0.23.0 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 18 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 19 | github.com/jackc/pgio v1.0.0 // indirect 20 | github.com/jackc/pgpassfile v1.0.0 // indirect 21 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 22 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect 23 | github.com/jackc/pgtype v1.14.3 // indirect 24 | github.com/mattn/go-runewidth v0.0.15 // indirect 25 | github.com/nsf/termbox-go v1.1.1 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | github.com/rivo/uniseg v0.4.7 // indirect 28 | github.com/spf13/pflag v1.0.5 // indirect 29 | golang.org/x/sys v0.20.0 // indirect 30 | golang.org/x/term v0.20.0 // indirect 31 | golang.org/x/text v0.15.0 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /internal/math/math.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | // Min returns minimum of two integers 4 | func Min(a, b int) int { 5 | if a > b { 6 | return b 7 | } 8 | return a 9 | } 10 | 11 | // Max returns maximum of two integers 12 | func Max(a, b int) int { 13 | if a > b { 14 | return a 15 | } 16 | return b 17 | } 18 | -------------------------------------------------------------------------------- /internal/math/math_test.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestMin(t *testing.T) { 9 | assert.Equal(t, 10, Min(15, 10)) 10 | assert.Equal(t, 10, Min(10, 15)) 11 | assert.Equal(t, 15, Min(15, 15)) 12 | } 13 | 14 | func TestMax(t *testing.T) { 15 | assert.Equal(t, 15, Max(15, 10)) 16 | assert.Equal(t, 15, Max(10, 15)) 17 | assert.Equal(t, 15, Max(15, 15)) 18 | } 19 | -------------------------------------------------------------------------------- /internal/postgres/connopts.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import "fmt" 4 | 5 | // ConnectionOptions defines connection options (used by all pgcenter subcommands). 6 | type ConnectionOptions struct { 7 | Host string 8 | Port int 9 | User string 10 | Dbname string 11 | } 12 | 13 | // ParseExtraArgs parses extra arguments passed in CLI and fills ConnectionOptions properties. 14 | func (c *ConnectionOptions) ParseExtraArgs(args []string) { 15 | for i := 0; i < len(args); i++ { 16 | if c.Dbname == "" { 17 | c.Dbname = args[i] 18 | } else { 19 | fmt.Printf("warning: extra command-line argument %s ignored\n", args[i]) 20 | } 21 | 22 | if i++; i >= len(args) { 23 | break 24 | } 25 | 26 | if c.User == "" { 27 | c.User = args[i] 28 | } else { 29 | fmt.Printf("warning: extra command-line argument %s ignored\n", args[i]) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/postgres/connopts_test.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | func TestConnectionOptions_ParseExtraArgs(t *testing.T) { 10 | var testcases = []struct { 11 | desc string 12 | opts *ConnectionOptions 13 | args []string 14 | wantdbname string 15 | wantuser string 16 | }{ 17 | { 18 | desc: "dbname and user are not specified", 19 | opts: &ConnectionOptions{Host: "127.0.0.1", Port: 1234}, 20 | args: []string{}, 21 | wantdbname: "", wantuser: "", 22 | }, 23 | { 24 | desc: "dbname specified as argument", 25 | opts: &ConnectionOptions{Host: "127.0.0.1", Port: 1234}, 26 | args: []string{"newdb"}, 27 | wantdbname: "newdb", wantuser: "", 28 | }, 29 | { 30 | desc: "dbname and user specified as argument", 31 | opts: &ConnectionOptions{Host: "127.0.0.1", Port: 1234}, 32 | args: []string{"newdb", "newuser"}, 33 | wantdbname: "newdb", wantuser: "newuser", 34 | }, 35 | { 36 | desc: "dbname specified as a parameter's values", 37 | opts: &ConnectionOptions{Host: "127.0.0.1", Port: 1234, Dbname: "postgres"}, 38 | args: []string{"newdb"}, 39 | wantdbname: "postgres", wantuser: "", 40 | }, 41 | { 42 | desc: "dbname and user are specified as a parameter's values", 43 | opts: &ConnectionOptions{Host: "127.0.0.1", Port: 1234, Dbname: "postgres", User: "postgres"}, 44 | args: []string{"newdb", "newuser"}, 45 | wantdbname: "postgres", wantuser: "postgres", 46 | }, 47 | } 48 | 49 | for i, tc := range testcases { 50 | t.Run(strconv.Itoa(i), func(t *testing.T) { 51 | tc.opts.ParseExtraArgs(tc.args) 52 | assert.Equal(t, tc.wantuser, tc.opts.User) 53 | assert.Equal(t, tc.wantdbname, tc.opts.Dbname) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/postgres/testing.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import "fmt" 4 | 5 | // NewTestConfig returns test config used for testing purposes. 6 | func NewTestConfig() (Config, error) { 7 | return NewConfig("127.0.0.1", 21914, "postgres", "pgcenter_fixtures") 8 | } 9 | 10 | // NewTestConnect returns default test connection used for testing purposes. 11 | func NewTestConnect() (*DB, error) { 12 | return NewTestConnectVersion(140000) 13 | } 14 | 15 | // NewTestConnectVersion connects to test Postgres of specific version. 16 | // Necessary Postgres instances have to be up and running on specified ports. 17 | func NewTestConnectVersion(version int) (*DB, error) { 18 | if version < 90400 || version >= 150000 { 19 | return nil, fmt.Errorf("unsupported version selected") 20 | } 21 | 22 | ports := map[int]int{ 23 | 140000: 21914, 24 | 130000: 21913, 25 | 120000: 21912, 26 | 110000: 21911, 27 | 100000: 21910, 28 | 90600: 21996, 29 | 90500: 21995, 30 | 90400: 21994, 31 | } 32 | 33 | config, err := NewConfig("127.0.0.1", ports[version], "postgres", "pgcenter_fixtures") 34 | if err != nil { 35 | return nil, err 36 | } 37 | return Connect(config) 38 | } 39 | -------------------------------------------------------------------------------- /internal/pretty/pretty.go: -------------------------------------------------------------------------------- 1 | package pretty 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Size returns human-readable value of passed size in bytes 8 | func Size(v float64) string { 9 | switch { 10 | case v == 0: 11 | return "0" 12 | case v < 1024: 13 | return fmt.Sprintf("%.0fB", v) 14 | case v < 1048576: 15 | return fmt.Sprintf("%.1fK", v/1024) 16 | case v < 1073741824: 17 | return fmt.Sprintf("%.1fM", v/1048576) 18 | case v < 1099511627776: 19 | return fmt.Sprintf("%.1fG", v/1073741824) 20 | default: 21 | return fmt.Sprintf("%.1fT", v/1099511627776) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/pretty/pretty_test.go: -------------------------------------------------------------------------------- 1 | package pretty 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestSize(t *testing.T) { 9 | testcases := []struct { 10 | v float64 11 | want string 12 | }{ 13 | {v: 0, want: "0"}, 14 | {v: 512, want: "512B"}, 15 | {v: 9425, want: "9.2K"}, 16 | {v: 425681, want: "415.7K"}, 17 | {v: 512548751, want: "488.8M"}, 18 | {v: 512254851486, want: "477.1G"}, 19 | {v: 512254851486475, want: "465.9T"}, 20 | } 21 | 22 | for _, tc := range testcases { 23 | assert.Equal(t, tc.want, Size(tc.v)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/query/activity.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatActivityDefault is the default query for getting stats from pg_stat_activity view. 5 | // - regexp_replace() removes extra spaces, tabs and newlines from queries. 6 | PgStatActivityDefault = "SELECT pid, host(client_addr) AS cl_addr, client_port AS cl_port, " + 7 | "datname, usename, application_name AS appname, backend_type, " + 8 | "wait_event_type AS wait_etype, wait_event, state, " + 9 | "date_trunc('seconds', clock_timestamp() - xact_start)::text AS xact_age, " + 10 | "date_trunc('seconds', clock_timestamp() - query_start)::text AS query_age, " + 11 | "date_trunc('seconds', clock_timestamp() - state_change)::text AS change_age, " + 12 | `regexp_replace(query, E'\\s+', ' ', 'g') AS query ` + 13 | "FROM pg_stat_activity " + 14 | "WHERE ((clock_timestamp() - xact_start) > '{{.QueryAgeThresh}}'::interval " + 15 | "OR (clock_timestamp() - query_start) > '{{.QueryAgeThresh}}'::interval) " + 16 | "{{ if .ShowNoIdle }} AND state != 'idle' {{ end }} ORDER BY pid DESC" 17 | 18 | // PgStatActivity96 queries for getting stats from pg_stat_activity view for versions 9.6.*. 19 | // - regexp_replace() removes extra spaces, tabs and newlines from queries. 20 | PgStatActivity96 = "SELECT pid, host(client_addr) AS cl_addr, client_port AS cl_port, datname, " + 21 | "usename, application_name AS appname, wait_event_type AS wait_etype, " + 22 | "wait_event, state, date_trunc('seconds', clock_timestamp() - xact_start)::text AS xact_age, " + 23 | "date_trunc('seconds', clock_timestamp() - query_start)::text AS query_age, " + 24 | "date_trunc('seconds', clock_timestamp() - state_change)::text AS change_age, " + 25 | `regexp_replace(query, E'\\s+', ' ', 'g') AS query ` + 26 | "FROM pg_stat_activity " + 27 | "WHERE ((clock_timestamp() - xact_start) > '{{.QueryAgeThresh}}'::interval " + 28 | "OR (clock_timestamp() - query_start) > '{{.QueryAgeThresh}}'::interval) " + 29 | "{{ if .ShowNoIdle }} AND state != 'idle' {{ end }} ORDER BY pid DESC" 30 | 31 | // PgStatActivity95 queries activity stats from pg_stat_activity view from versions for 9.5.* and later. 32 | // - regexp_replace() removes extra spaces, tabs and newlines from queries. 33 | PgStatActivity95 = "SELECT pid, host(client_addr) AS cl_addr, client_port AS cl_port, datname, " + 34 | "usename, application_name AS appname, waiting, state, " + 35 | "date_trunc('seconds', clock_timestamp() - xact_start)::text AS xact_age, " + 36 | "date_trunc('seconds', clock_timestamp() - query_start)::text AS query_age, " + 37 | "date_trunc('seconds', clock_timestamp() - state_change)::text AS change_age, " + 38 | `regexp_replace(query, E'\\s+', ' ', 'g') AS query ` + 39 | "FROM pg_stat_activity " + 40 | "WHERE ((clock_timestamp() - xact_start) > '{{.QueryAgeThresh}}'::interval " + 41 | "OR (clock_timestamp() - query_start) > '{{.QueryAgeThresh}}'::interval) " + 42 | "{{ if .ShowNoIdle }} AND state != 'idle' {{ end }} ORDER BY pid DESC" 43 | ) 44 | 45 | // SelectStatActivityQuery returns proper query and number of columns, depending on Postgres version. 46 | func SelectStatActivityQuery(version int) (string, int) { 47 | switch { 48 | case version < 90600: 49 | return PgStatActivity95, 12 50 | case version < 100000: 51 | return PgStatActivity96, 13 52 | default: 53 | return PgStatActivityDefault, 14 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/query/activity_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestSelectStatActivityQuery(t *testing.T) { 11 | testcases := []struct { 12 | version int 13 | wantQ string 14 | wantN int 15 | }{ 16 | {version: 90500, wantQ: PgStatActivity95, wantN: 12}, 17 | {version: 90600, wantQ: PgStatActivity96, wantN: 13}, 18 | {version: 100000, wantQ: PgStatActivityDefault, wantN: 14}, 19 | } 20 | 21 | for _, tc := range testcases { 22 | gotQ, gotN := SelectStatActivityQuery(tc.version) 23 | assert.Equal(t, tc.wantQ, gotQ) 24 | assert.Equal(t, tc.wantN, gotN) 25 | } 26 | } 27 | 28 | func Test_StatActivityQueries(t *testing.T) { 29 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 30 | 31 | for _, version := range versions { 32 | t.Run(fmt.Sprintf("pg_stat_activity/%d", version), func(t *testing.T) { 33 | tmpl, _ := SelectStatActivityQuery(version) 34 | 35 | opts := NewOptions(version, "f", "off", 256, "public") 36 | q, err := Format(tmpl, opts) 37 | assert.NoError(t, err) 38 | 39 | conn, err := postgres.NewTestConnectVersion(version) 40 | assert.NoError(t, err) 41 | 42 | _, err = conn.Exec(q) 43 | assert.NoError(t, err) 44 | 45 | conn.Close() 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/query/common_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestSelectActivityAutovacuumQuery(t *testing.T) { 10 | testcases := []struct { 11 | version int 12 | want string 13 | }{ 14 | {version: 90300, want: SelectAutovacuumPG93}, 15 | {version: 90400, want: SelectAutovacuumDefault}, 16 | {version: 90500, want: SelectAutovacuumDefault}, 17 | {version: 90600, want: SelectAutovacuumDefault}, 18 | {version: 100000, want: SelectAutovacuumDefault}, 19 | {version: 110000, want: SelectAutovacuumDefault}, 20 | {version: 120000, want: SelectAutovacuumDefault}, 21 | {version: 130000, want: SelectAutovacuumDefault}, 22 | } 23 | 24 | for _, tc := range testcases { 25 | assert.Equal(t, tc.want, SelectActivityAutovacuumQuery(tc.version)) 26 | } 27 | } 28 | 29 | func TestSelectActivityActivityQuery(t *testing.T) { 30 | testcases := []struct { 31 | version int 32 | want string 33 | }{ 34 | {version: 90300, want: SelectActivityPG93}, 35 | {version: 90400, want: SelectActivityPG95}, 36 | {version: 90500, want: SelectActivityPG95}, 37 | {version: 90600, want: SelectActivityPG96}, 38 | {version: 100000, want: SelectActivityDefault}, 39 | {version: 110000, want: SelectActivityDefault}, 40 | {version: 120000, want: SelectActivityDefault}, 41 | {version: 130000, want: SelectActivityDefault}, 42 | } 43 | 44 | for _, tc := range testcases { 45 | assert.Equal(t, tc.want, SelectActivityActivityQuery(tc.version)) 46 | } 47 | } 48 | 49 | func TestSelectActivityStatementsQuery(t *testing.T) { 50 | testcases := []struct { 51 | version int 52 | want string 53 | }{ 54 | {version: 120000, want: SelectActivityStatementsPG12}, 55 | {version: 130000, want: SelectActivityStatementsLatest}, 56 | } 57 | 58 | for _, tc := range testcases { 59 | assert.Equal(t, tc.want, SelectActivityStatementsQuery(tc.version)) 60 | } 61 | } 62 | 63 | func Test_CommonQueries(t *testing.T) { 64 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 65 | 66 | queries := []struct { 67 | query string 68 | args []interface{} 69 | }{ 70 | {query: GetSetting, args: []interface{}{"work_mem"}}, 71 | {query: GetRecoveryStatus}, 72 | {query: GetUptime}, 73 | {query: CheckSchemaExists, args: []interface{}{"public"}}, 74 | {query: GetExtensionSchema, args: []interface{}{"plpgsql"}}, 75 | {query: GetAllSettings}, 76 | {query: ExecReloadConf}, 77 | {query: ExecResetStats}, 78 | {query: SelectCommonProperties}, 79 | } 80 | 81 | t.Run("common_queries", func(t *testing.T) { 82 | for _, version := range versions { 83 | for _, query := range queries { 84 | conn, err := postgres.NewTestConnectVersion(version) 85 | assert.NoError(t, err) 86 | 87 | _, err = conn.Exec(query.query, query.args...) 88 | assert.NoError(t, err) 89 | 90 | conn.Close() 91 | } 92 | } 93 | }) 94 | 95 | // GetCurrentLogfile available since Postgres 10 96 | t.Run("current_logfiles", func(t *testing.T) { 97 | for _, version := range versions[3:] { 98 | conn, err := postgres.NewTestConnectVersion(version) 99 | assert.NoError(t, err) 100 | _, err = conn.Exec(GetCurrentLogfile) 101 | assert.NoError(t, err) 102 | conn.Close() 103 | } 104 | }) 105 | 106 | t.Run("activity_activity_queries", func(t *testing.T) { 107 | for _, version := range versions { 108 | conn, err := postgres.NewTestConnectVersion(version) 109 | assert.NoError(t, err) 110 | 111 | _, err = conn.Exec(SelectActivityActivityQuery(version)) 112 | assert.NoError(t, err) 113 | 114 | conn.Close() 115 | } 116 | }) 117 | 118 | t.Run("activity_autovacuum_queries", func(t *testing.T) { 119 | for _, version := range versions { 120 | conn, err := postgres.NewTestConnectVersion(version) 121 | assert.NoError(t, err) 122 | 123 | _, err = conn.Exec(SelectActivityAutovacuumQuery(version)) 124 | assert.NoError(t, err) 125 | 126 | conn.Close() 127 | } 128 | }) 129 | 130 | } 131 | -------------------------------------------------------------------------------- /internal/query/databases.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatDatabaseGeneralDefault defines default query for getting general databases' stats from pg_stat_database view. 5 | PgStatDatabaseGeneralDefault = "SELECT datname, numbackends AS backends_total, " + 6 | "coalesce(xact_commit, 0) AS commits, coalesce(xact_rollback, 0) AS rollbacks, " + 7 | `coalesce(blks_read * (SELECT current_setting('block_size')::int / 1024), 0) AS "read,KiB", ` + 8 | "coalesce(blks_hit, 0) AS hits, coalesce(tup_returned, 0) AS returned, " + 9 | "coalesce(tup_fetched, 0) AS fetched, coalesce(tup_inserted, 0) AS inserts, " + 10 | "coalesce(tup_updated, 0) AS updates, coalesce(tup_deleted, 0) AS deletes, " + 11 | "coalesce(conflicts, 0) AS conflicts, coalesce(deadlocks, 0) AS deadlocks, " + 12 | "coalesce(checksum_failures, 0) AS csum_fails, coalesce(temp_files, 0) AS temp_files, " + 13 | `coalesce(temp_bytes, 0) AS temp_bytes, coalesce(blk_read_time, 0)::numeric(20,2) AS "read,ms", ` + 14 | `coalesce(blk_write_time, 0)::numeric(20,2) AS "write,ms", ` + 15 | "date_trunc('seconds', now() - stats_reset)::text AS stats_age " + 16 | "FROM pg_stat_database ORDER BY datname DESC" 17 | 18 | // PgStatDatabaseGeneralPG11 defines query for getting general databases' stats from pg_stat_database view for versions 11 and older. 19 | PgStatDatabaseGeneralPG11 = "SELECT datname, numbackends AS backends_total, " + 20 | "coalesce(xact_commit, 0) AS commits, coalesce(xact_rollback, 0) AS rollbacks, " + 21 | `coalesce(blks_read * (SELECT current_setting('block_size')::int / 1024), 0) AS "read,KiB", ` + 22 | "coalesce(blks_hit, 0) AS hits, coalesce(tup_returned, 0) AS returned, " + 23 | "coalesce(tup_fetched, 0) AS fetched, coalesce(tup_inserted, 0) AS inserts, " + 24 | "coalesce(tup_updated, 0) AS updates, coalesce(tup_deleted, 0) AS deletes, " + 25 | "coalesce(conflicts, 0) AS conflicts, coalesce(deadlocks, 0) AS deadlocks, " + 26 | "coalesce(temp_files, 0) AS temp_files, coalesce(temp_bytes, 0) AS temp_bytes, " + 27 | `coalesce(blk_read_time, 0)::numeric(20,2) AS "read,ms", ` + 28 | `coalesce(blk_write_time, 0)::numeric(20,2) AS "write,ms", ` + 29 | "date_trunc('seconds', now() - stats_reset)::text AS stats_age " + 30 | "FROM pg_stat_database ORDER BY datname DESC" 31 | 32 | // PgStatDatabaseSessionsDefault defines query for getting sessions stats from pg_stat_database view (available since Postgres 14). 33 | PgStatDatabaseSessionsDefault = "SELECT datname, numbackends AS backends_total, " + 34 | "date_trunc('seconds', session_time / 1000 * '1 second'::interval)::text AS session_total, " + 35 | "date_trunc('seconds', active_time / 1000 * '1 second'::interval)::text AS active_total, " + 36 | "date_trunc('seconds', idle_in_transaction_time / 1000 * '1 second'::interval)::text AS idle_xact_total, " + 37 | `session_time AS "session,ms", ` + 38 | `active_time AS "active,ms", ` + 39 | `idle_in_transaction_time AS "idle_xact,ms", ` + 40 | "sessions, sessions_abandoned AS abandoned, sessions_fatal AS fatal, sessions_killed AS killed " + 41 | "FROM pg_stat_database" 42 | ) 43 | 44 | // SelectStatDatabaseGeneralQuery returns proper query, number of columns and diff interval depending on Postgres version. 45 | func SelectStatDatabaseGeneralQuery(version int) (string, int, [2]int) { 46 | switch { 47 | case version < 120000: 48 | return PgStatDatabaseGeneralPG11, 18, [2]int{2, 16} 49 | default: 50 | return PgStatDatabaseGeneralDefault, 19, [2]int{2, 17} 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/query/databases_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestSelectStatDatabaseQuery(t *testing.T) { 11 | testcases := []struct { 12 | version int 13 | wantQ string 14 | wantN int 15 | wantD [2]int 16 | }{ 17 | {version: 90500, wantQ: PgStatDatabaseGeneralPG11, wantN: 18, wantD: [2]int{2, 16}}, 18 | {version: 90600, wantQ: PgStatDatabaseGeneralPG11, wantN: 18, wantD: [2]int{2, 16}}, 19 | {version: 100000, wantQ: PgStatDatabaseGeneralPG11, wantN: 18, wantD: [2]int{2, 16}}, 20 | {version: 110000, wantQ: PgStatDatabaseGeneralPG11, wantN: 18, wantD: [2]int{2, 16}}, 21 | {version: 120000, wantQ: PgStatDatabaseGeneralDefault, wantN: 19, wantD: [2]int{2, 17}}, 22 | {version: 130000, wantQ: PgStatDatabaseGeneralDefault, wantN: 19, wantD: [2]int{2, 17}}, 23 | } 24 | 25 | for _, tc := range testcases { 26 | gotQ, gotN, gotD := SelectStatDatabaseGeneralQuery(tc.version) 27 | assert.Equal(t, tc.wantQ, gotQ) 28 | assert.Equal(t, tc.wantN, gotN) 29 | assert.Equal(t, tc.wantD, gotD) 30 | } 31 | } 32 | 33 | func Test_SelectStatDatabaseGeneralQuery(t *testing.T) { 34 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 35 | 36 | for _, version := range versions { 37 | t.Run(fmt.Sprintf("pg_stat_database/general/%d", version), func(t *testing.T) { 38 | tmpl, _, _ := SelectStatDatabaseGeneralQuery(version) 39 | 40 | opts := NewOptions(version, "f", "off", 256, "public") 41 | q, err := Format(tmpl, opts) 42 | assert.NoError(t, err) 43 | 44 | conn, err := postgres.NewTestConnectVersion(version) 45 | assert.NoError(t, err) 46 | 47 | _, err = conn.Exec(q) 48 | assert.NoError(t, err) 49 | 50 | conn.Close() 51 | }) 52 | } 53 | 54 | for _, version := range []int{140000} { 55 | t.Run(fmt.Sprintf("pg_stat_database/sessions/%d", version), func(t *testing.T) { 56 | tmpl := PgStatDatabaseSessionsDefault 57 | 58 | opts := NewOptions(version, "f", "off", 256, "public") 59 | q, err := Format(tmpl, opts) 60 | assert.NoError(t, err) 61 | 62 | conn, err := postgres.NewTestConnectVersion(version) 63 | assert.NoError(t, err) 64 | 65 | _, err = conn.Exec(q) 66 | assert.NoError(t, err) 67 | 68 | conn.Close() 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/query/functions.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatFunctionsDefault is the default query for getting stats from pg_stat_user_functions view 5 | PgStatFunctionsDefault = "SELECT funcid, schemaname ||'.'||funcname AS function, " + 6 | "calls AS calls_total, calls AS calls, " + 7 | "date_trunc('seconds', total_time / 1000 * '1 second'::interval)::text AS total, " + 8 | "date_trunc('seconds', self_time / 1000 * '1 second'::interval)::text AS self, " + 9 | `round((total_time / greatest(calls, 1))::numeric(20,2), 4)::text AS "total_avg,ms", ` + 10 | `round((self_time / greatest(calls, 1))::numeric(20,2), 4)::text AS "self_avg,ms" ` + 11 | "FROM pg_stat_user_functions ORDER BY funcid DESC" 12 | ) 13 | -------------------------------------------------------------------------------- /internal/query/functions_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_StatFunctionsQueries(t *testing.T) { 11 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 12 | 13 | for _, version := range versions { 14 | t.Run(fmt.Sprintf("pg_stat_user_functions/%d", version), func(t *testing.T) { 15 | tmpl := PgStatFunctionsDefault 16 | 17 | opts := NewOptions(version, "f", "off", 256, "public") 18 | q, err := Format(tmpl, opts) 19 | assert.NoError(t, err) 20 | 21 | conn, err := postgres.NewTestConnectVersion(version) 22 | assert.NoError(t, err) 23 | 24 | _, err = conn.Exec(q) 25 | assert.NoError(t, err) 26 | 27 | conn.Close() 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/query/indexes.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatIndexesDefault defines default query for getting indexes' stats from pg_stat_all_indexes and pg_statio_all_indexes views. 5 | PgStatIndexesDefault = "SELECT s.schemaname ||'.'|| s.relname ||'.'|| s.indexrelname AS index, " + 6 | "coalesce(s.idx_scan, 0) AS scan, coalesce(s.idx_tup_read, 0) AS tuples_read, " + 7 | "coalesce(s.idx_tup_fetch, 0) AS tuples_fetch, " + 8 | "coalesce(i.idx_blks_hit, 0) AS hit, " + 9 | `coalesce(i.idx_blks_read * (SELECT current_setting('block_size')::int / 1024), 0) AS "read,KiB" ` + 10 | "FROM pg_stat_{{.ViewType}}_indexes s, pg_statio_{{.ViewType}}_indexes i " + 11 | "WHERE s.indexrelid = i.indexrelid ORDER BY (s.schemaname ||'.'|| s.relname ||'.'|| s.indexrelname) DESC" 12 | ) 13 | -------------------------------------------------------------------------------- /internal/query/indexes_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_StatIndexesQueries(t *testing.T) { 11 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 12 | 13 | for _, version := range versions { 14 | t.Run(fmt.Sprintf("pg_stat_indexes/%d", version), func(t *testing.T) { 15 | tmpl := PgStatIndexesDefault 16 | 17 | opts := NewOptions(version, "f", "off", 256, "public") 18 | q, err := Format(tmpl, opts) 19 | assert.NoError(t, err) 20 | 21 | conn, err := postgres.NewTestConnectVersion(version) 22 | assert.NoError(t, err) 23 | 24 | _, err = conn.Exec(q) 25 | assert.NoError(t, err) 26 | 27 | conn.Close() 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/query/pgcenter_schema_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_QueryPgcenterSchema(t *testing.T) { 11 | queries := []string{ 12 | "SELECT * FROM pgcenter.sys_proc_diskstats", 13 | "SELECT * FROM pgcenter.sys_proc_loadavg", 14 | "SELECT * FROM pgcenter.sys_proc_meminfo", 15 | "SELECT * FROM pgcenter.sys_proc_netdev", 16 | "SELECT * FROM pgcenter.sys_proc_stat", 17 | "SELECT * FROM pgcenter.sys_proc_uptime", 18 | "SELECT * FROM pgcenter.sys_proc_mounts", 19 | } 20 | 21 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 22 | 23 | for _, version := range versions { 24 | t.Run(fmt.Sprintf("query-pgcenter-schema/%d", version), func(t *testing.T) { 25 | conn, err := postgres.NewTestConnectVersion(version) 26 | assert.NoError(t, err) 27 | 28 | for _, q := range queries { 29 | _, err = conn.Exec(q) 30 | assert.NoError(t, err) 31 | } 32 | 33 | conn.Close() 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/query/progress_analyze.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatProgressAnalyzeDefault defines default query for getting stats from pg_stat_progress_analyze view. 5 | PgStatProgressAnalyzeDefault = "SELECT " + 6 | "a.pid, date_trunc('seconds', clock_timestamp() - xact_start)::text AS xact_age, p.datname, p.relid::regclass AS relation, " + 7 | "a.state, coalesce((a.wait_event_type ||'.'|| a.wait_event), 'f') AS waiting, p.phase, " + 8 | `p.sample_blks_total * (SELECT current_setting('block_size')::int / 1024) AS "sample_size,KiB", ` + 9 | `round(100 * p.sample_blks_scanned / greatest(p.sample_blks_total,1), 2)::text AS "scanned,%", ` + 10 | `p.ext_stats_total ||'/'|| p.ext_stats_computed::text AS "ext_total/done", ` + 11 | `p.child_tables_total||'/'|| round(100 * p.child_tables_done / greatest(p.child_tables_total, 1), 2)::text AS "child_total/done,%", ` + 12 | "current_child_table_relid::regclass AS child_in_progress " + 13 | "FROM pg_stat_progress_analyze p INNER JOIN pg_stat_activity a ON p.pid = a.pid " + 14 | "WHERE a.pid <> pg_backend_pid() ORDER BY a.pid DESC" 15 | ) 16 | -------------------------------------------------------------------------------- /internal/query/progress_analyze_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_StatProgressAnalyzeQueries(t *testing.T) { 11 | versions := []int{140000} 12 | 13 | for _, version := range versions { 14 | t.Run(fmt.Sprintf("pg_stat_progress_analyze/%d", version), func(t *testing.T) { 15 | tmpl := PgStatProgressAnalyzeDefault 16 | 17 | opts := NewOptions(version, "f", "off", 256, "public") 18 | q, err := Format(tmpl, opts) 19 | assert.NoError(t, err) 20 | 21 | conn, err := postgres.NewTestConnectVersion(version) 22 | assert.NoError(t, err) 23 | 24 | _, err = conn.Exec(q) 25 | assert.NoError(t, err) 26 | 27 | conn.Close() 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/query/progress_basebackup.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatProgressBasebackupDefault defines default query for getting stats from pg_stat_progress_basebackup view. 5 | PgStatProgressBasebackupDefault = "SELECT " + 6 | "a.pid, host(a.client_addr) AS started_from, " + 7 | "to_char(backend_start, 'YYYY-MM-DD HH24:MI:SS') AS started_at, " + 8 | "date_trunc('seconds', clock_timestamp() - backend_start)::text AS duration, a.state, " + 9 | "coalesce((a.wait_event_type ||'.'|| a.wait_event), 'f') AS waiting, p.phase, " + 10 | `p.backup_total / 1024 AS "size_total,KiB", ` + 11 | `round(100 * p.backup_streamed / greatest(p.backup_total,1), 2)::text AS "streamed,%", ` + 12 | `coalesce(p.backup_streamed / 1024, 0) AS "streamed,KiB", ` + 13 | `p.tablespaces_total||'/'|| p.tablespaces_streamed::text AS "tablespaces_total/streamed" ` + 14 | "FROM pg_stat_progress_basebackup p INNER JOIN pg_stat_activity a ON p.pid = a.pid " + 15 | "WHERE a.pid <> pg_backend_pid() ORDER BY a.pid DESC" 16 | ) 17 | -------------------------------------------------------------------------------- /internal/query/progress_basebackup_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_StatProgressBasebackupQueries(t *testing.T) { 11 | versions := []int{140000} 12 | 13 | for _, version := range versions { 14 | t.Run(fmt.Sprintf("pg_stat_progress_basebackup/%d", version), func(t *testing.T) { 15 | tmpl := PgStatProgressBasebackupDefault 16 | 17 | opts := NewOptions(version, "f", "off", 256, "public") 18 | q, err := Format(tmpl, opts) 19 | assert.NoError(t, err) 20 | 21 | conn, err := postgres.NewTestConnectVersion(version) 22 | assert.NoError(t, err) 23 | 24 | _, err = conn.Exec(q) 25 | assert.NoError(t, err) 26 | 27 | conn.Close() 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/query/progress_cluster.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatProgressClusterDefault defines default query for getting stats from pg_stat_progress_cluster view. 5 | PgStatProgressClusterDefault = "SELECT a.pid, date_trunc('seconds', clock_timestamp() - xact_start)::text AS xact_age, " + 6 | "p.datname, p.relid::regclass AS relation, p.cluster_index_relid::regclass AS index, a.state, " + 7 | "coalesce((a.wait_event_type ||'.'|| a.wait_event), 'f') AS waiting, p.phase, " + 8 | `p.heap_blks_total * (SELECT current_setting('block_size')::int / 1024) AS "size_total,KiB", ` + 9 | `round(100 * p.heap_blks_scanned / greatest(p.heap_blks_total,1), 2)::text AS "scanned_total,%", ` + 10 | "coalesce(p.heap_tuples_scanned, 0) AS tuples_scanned, coalesce(p.heap_tuples_written, 0) AS tuples_written, a.query " + 11 | "FROM pg_stat_progress_cluster p INNER JOIN pg_stat_activity a ON p.pid = a.pid " + 12 | "WHERE a.pid <> pg_backend_pid() ORDER BY a.pid DESC" 13 | ) 14 | -------------------------------------------------------------------------------- /internal/query/progress_cluster_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_StatProgressClusterQueries(t *testing.T) { 11 | versions := []int{120000, 130000, 140000} 12 | 13 | for _, version := range versions { 14 | t.Run(fmt.Sprintf("pg_stat_progress_cluster/%d", version), func(t *testing.T) { 15 | tmpl := PgStatProgressClusterDefault 16 | 17 | opts := NewOptions(version, "f", "off", 256, "public") 18 | q, err := Format(tmpl, opts) 19 | assert.NoError(t, err) 20 | 21 | conn, err := postgres.NewTestConnectVersion(version) 22 | assert.NoError(t, err) 23 | 24 | _, err = conn.Exec(q) 25 | assert.NoError(t, err) 26 | 27 | conn.Close() 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/query/progress_copy.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatProgressCopyDefault defines default query for getting stats from pg_stat_progress_copy view. 5 | PgStatProgressCopyDefault = "SELECT a.pid, date_trunc('seconds', clock_timestamp() - xact_start)::text AS xact_age, " + 6 | "p.datname, p.relid::regclass AS relation, a.state, " + 7 | "coalesce((a.wait_event_type ||'.'|| a.wait_event), 'f') AS waiting, p.command, p.type, " + 8 | `pg_relation_size(p.relid) / 1024 AS "size_total,KiB", ` + 9 | `p.bytes_total / 1024 AS "source_total,KiB", p.bytes_processed / 1024 AS "processed,KiB", ` + 10 | `round(100 * p.bytes_processed / nullif(p.bytes_total, 0), 2)::text AS "processed,%", ` + 11 | "p.tuples_processed, p.tuples_excluded " + 12 | "FROM pg_stat_progress_copy p INNER JOIN pg_stat_activity a ON p.pid = a.pid " + 13 | "WHERE a.pid <> pg_backend_pid() AND NOT EXISTS (SELECT 1 FROM pg_locks WHERE relation = p.relid AND mode = 'AccessExclusiveLock' AND granted) " + 14 | "ORDER BY a.pid DESC" 15 | ) 16 | -------------------------------------------------------------------------------- /internal/query/progress_copy_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_StatProgressCopyQueries(t *testing.T) { 11 | versions := []int{140000} 12 | 13 | for _, version := range versions { 14 | t.Run(fmt.Sprintf("pg_stat_progress_copy/%d", version), func(t *testing.T) { 15 | tmpl := PgStatProgressCopyDefault 16 | 17 | opts := NewOptions(version, "f", "off", 256, "public") 18 | q, err := Format(tmpl, opts) 19 | assert.NoError(t, err) 20 | 21 | conn, err := postgres.NewTestConnectVersion(version) 22 | assert.NoError(t, err) 23 | 24 | _, err = conn.Exec(q) 25 | assert.NoError(t, err) 26 | 27 | conn.Close() 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/query/progress_create_index.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatProgressCreateIndexDefault defines default query for getting stats from pg_stat_progress_cluster view. 5 | PgStatProgressCreateIndexDefault = "SELECT a.pid, date_trunc('seconds', clock_timestamp() - xact_start)::text AS xact_age, " + 6 | "p.datname, p.relid::regclass AS relation, p.index_relid::regclass AS index, a.state, " + 7 | "coalesce((a.wait_event_type ||'.'|| a.wait_event), 'f') AS waiting, p.phase, current_locker_pid AS locker_pid, " + 8 | "lockers_total ||'/'|| lockers_done AS lockers, " + 9 | `p.blocks_total * (SELECT current_setting('block_size')::int / 1024) ||'/'|| round(100 * p.blocks_done / greatest(p.blocks_total, 1), 2)::text AS "size_total/done,%", ` + 10 | `p.tuples_total ||'/'|| round(100 * p.tuples_done / greatest(p.tuples_total, 1), 2)::text AS "tuples_total/done,%", ` + 11 | `p.partitions_total ||'/'|| round(100 * p.partitions_done / greatest(p.partitions_total, 1), 2)::text AS "parts_total/done,%", a.query ` + 12 | "FROM pg_stat_progress_create_index p INNER JOIN pg_stat_activity a ON p.pid = a.pid " + 13 | "WHERE a.pid <> pg_backend_pid() ORDER BY a.pid DESC" 14 | ) 15 | -------------------------------------------------------------------------------- /internal/query/progress_create_index_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_StatProgressCreateIndexQueries(t *testing.T) { 11 | versions := []int{120000, 130000, 140000} 12 | 13 | for _, version := range versions { 14 | t.Run(fmt.Sprintf("pg_stat_progress_create_index/%d", version), func(t *testing.T) { 15 | tmpl := PgStatProgressCreateIndexDefault 16 | 17 | opts := NewOptions(version, "f", "off", 256, "public") 18 | q, err := Format(tmpl, opts) 19 | assert.NoError(t, err) 20 | 21 | conn, err := postgres.NewTestConnectVersion(version) 22 | assert.NoError(t, err) 23 | 24 | _, err = conn.Exec(q) 25 | assert.NoError(t, err) 26 | 27 | conn.Close() 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/query/progress_vacuum.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatProgressVacuumDefault defines default query for getting stats from pg_stat_progress_vacuum view. 5 | PgStatProgressVacuumDefault = "SELECT a.pid, date_trunc('seconds', clock_timestamp() - xact_start)::text AS xact_age, " + 6 | "v.datname, v.relid::regclass AS relation, a.state, coalesce((a.wait_event_type ||'.'|| a.wait_event), 'f') AS waiting, " + 7 | `v.phase, v.heap_blks_total * (SELECT current_setting('block_size')::int / 1024) AS "size_total,KiB", ` + 8 | `round(100 * v.heap_blks_scanned / v.heap_blks_total, 2)::text AS "scanned_total,%", ` + 9 | `round(100 * v.heap_blks_vacuumed / v.heap_blks_total, 2)::text AS "vacuumed_total,%", ` + 10 | `coalesce(v.heap_blks_scanned * (SELECT current_setting('block_size')::int / 1024), 0) AS "scanned,KiB", ` + 11 | `coalesce(v.heap_blks_vacuumed * (SELECT current_setting('block_size')::int / 1024), 0) AS "vacuumed,KiB", a.query ` + 12 | "FROM pg_stat_progress_vacuum v RIGHT JOIN pg_stat_activity a ON v.pid = a.pid " + 13 | "WHERE (a.query ~* '^autovacuum:' OR a.query ~* '^vacuum') AND a.pid <> pg_backend_pid() ORDER BY a.pid DESC" 14 | ) 15 | -------------------------------------------------------------------------------- /internal/query/progress_vacuum_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_StatProgressVacuumQueries(t *testing.T) { 11 | versions := []int{90600, 100000, 110000, 120000, 130000, 140000} 12 | 13 | for _, version := range versions { 14 | t.Run(fmt.Sprintf("pg_stat_progress_vacuum/%d", version), func(t *testing.T) { 15 | tmpl := PgStatProgressVacuumDefault 16 | 17 | opts := NewOptions(version, "f", "off", 256, "public") 18 | q, err := Format(tmpl, opts) 19 | assert.NoError(t, err) 20 | 21 | conn, err := postgres.NewTestConnectVersion(version) 22 | assert.NoError(t, err) 23 | 24 | _, err = conn.Exec(q) 25 | assert.NoError(t, err) 26 | 27 | conn.Close() 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/query/query.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "text/template" 7 | ) 8 | 9 | const ( 10 | PostgresV94 = 90400 11 | PostgresV95 = 90500 12 | PostgresV96 = 90600 13 | PostgresV10 = 100000 14 | PostgresV11 = 110000 15 | PostgresV12 = 120000 16 | PostgresV13 = 130000 17 | PostgresV14 = 140000 18 | ) 19 | 20 | // Options contains queries' settings that used depending on user preferences. 21 | type Options struct { 22 | Version int // Postgres version (numeric format) 23 | Recovery string // Recovery state 24 | GucTrackCommitTS string // Value of track_commit_timestamp GUC 25 | ViewType string // Show stats including system tables/indexes 26 | WalFunction1 string // Use old pg_xlog_* or newer pg_wal_* functions 27 | WalFunction2 string // Use old pg_xlog_* or newer pg_wal_* functions 28 | QueryAgeThresh string // Show only queries with duration more than specified 29 | BackendState string // Backend state's selector for cancel/terminate function 30 | ShowNoIdle bool // don't show IDLEs, background workers) 31 | PGSSSchema string // Schema where pg_stat_statements installed 32 | PgSSQueryLen int // Specify the length of query to show in pg_stat_statements 33 | PgSSQueryLenFn string // Specify exact func to truncating query 34 | } 35 | 36 | // NewOptions creates query options used for queries customization depending on Postgres version and other important settings. 37 | func NewOptions(version int, recovery string, track string, querylen int, pgssSchema string) Options { 38 | opts := Options{ 39 | Version: version, 40 | Recovery: recovery, 41 | GucTrackCommitTS: track, 42 | ViewType: "user", // System tables and indexes aren't shown by default 43 | QueryAgeThresh: "00:00:00.0", // Don't filter queries by age 44 | ShowNoIdle: true, // Don't show idle clients and background workers 45 | PGSSSchema: pgssSchema, 46 | PgSSQueryLen: querylen, 47 | } 48 | 49 | opts.WalFunction1, opts.WalFunction2 = selectWalFunctions(opts.Version, opts.Recovery) 50 | 51 | // Define length limit for pg_stat_statement.query. 52 | if opts.PgSSQueryLen > 0 { 53 | opts.PgSSQueryLenFn = fmt.Sprintf("left(p.query, %d)", opts.PgSSQueryLen) 54 | } else { 55 | opts.PgSSQueryLenFn = "p.query" 56 | } 57 | 58 | return opts 59 | } 60 | 61 | // selectWalFunctions returns proper function names for getting WAL locations. 62 | // 1. WAL-related functions have been renamed in Postgres 10, functions' names between 9.x and 10 are different. 63 | // 2. Depending on recovery status, for obtaining WAL location different functions have to be used. 64 | func selectWalFunctions(version int, recovery string) (string, string) { 65 | var fn1, fn2 string 66 | switch { 67 | case version < PostgresV10: 68 | fn1 = "pg_xlog_location_diff" 69 | if recovery == "f" { 70 | fn2 = "pg_current_xlog_location" 71 | } else { 72 | fn2 = "pg_last_xlog_receive_location" 73 | } 74 | default: 75 | fn1 = "pg_wal_lsn_diff" 76 | if recovery == "f" { 77 | fn2 = "pg_current_wal_lsn" 78 | } else { 79 | fn2 = "pg_last_wal_receive_lsn" 80 | } 81 | } 82 | return fn1, fn2 83 | } 84 | 85 | // Format transforms query's template to a particular query. 86 | func Format(tmpl string, o Options) (string, error) { 87 | t, err := template.New("query").Parse(tmpl) 88 | if err != nil { 89 | return "", err 90 | } 91 | 92 | buf := &bytes.Buffer{} 93 | err = t.Execute(buf, o) 94 | if err != nil { 95 | return "", err 96 | } 97 | 98 | return buf.String(), nil 99 | } 100 | -------------------------------------------------------------------------------- /internal/query/replication_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestSelectStatReplicationQuery(t *testing.T) { 11 | testcases := []struct { 12 | version int 13 | track bool 14 | wantQ string 15 | wantN int 16 | }{ 17 | {version: 90500, track: false, wantQ: PgStatReplication96, wantN: 12}, 18 | {version: 90500, track: true, wantQ: PgStatReplication96Extended, wantN: 14}, 19 | {version: 90600, track: false, wantQ: PgStatReplication96, wantN: 12}, 20 | {version: 90600, track: true, wantQ: PgStatReplication96Extended, wantN: 14}, 21 | {version: 100000, track: false, wantQ: PgStatReplicationDefault, wantN: 15}, 22 | {version: 100000, track: true, wantQ: PgStatReplicationExtended, wantN: 17}, 23 | {version: 110000, track: false, wantQ: PgStatReplicationDefault, wantN: 15}, 24 | {version: 110000, track: true, wantQ: PgStatReplicationExtended, wantN: 17}, 25 | {version: 120000, track: false, wantQ: PgStatReplicationDefault, wantN: 15}, 26 | {version: 120000, track: true, wantQ: PgStatReplicationExtended, wantN: 17}, 27 | {version: 130000, track: false, wantQ: PgStatReplicationDefault, wantN: 15}, 28 | {version: 130000, track: true, wantQ: PgStatReplicationExtended, wantN: 17}, 29 | } 30 | 31 | for _, tc := range testcases { 32 | gotQ, gotN := SelectStatReplicationQuery(tc.version, tc.track) 33 | assert.Equal(t, tc.wantQ, gotQ) 34 | assert.Equal(t, tc.wantN, gotN) 35 | } 36 | } 37 | 38 | func Test_StatReplicationQueries(t *testing.T) { 39 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 40 | 41 | for _, version := range versions { 42 | t.Run(fmt.Sprintf("pg_stat_replication/%d", version), func(t *testing.T) { 43 | tmpl1, _ := SelectStatReplicationQuery(version, false) 44 | tmpl2, _ := SelectStatReplicationQuery(version, true) 45 | 46 | opts := NewOptions(version, "f", "off", 256, "public") 47 | q1, err := Format(tmpl1, opts) 48 | assert.NoError(t, err) 49 | 50 | q2, err := Format(tmpl2, opts) 51 | assert.NoError(t, err) 52 | 53 | conn, err := postgres.NewTestConnectVersion(version) 54 | assert.NoError(t, err) 55 | 56 | _, err = conn.Exec(q1) 57 | assert.NoError(t, err) 58 | 59 | _, err = conn.Exec(q2) 60 | assert.NoError(t, err) 61 | 62 | conn.Close() 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/query/sizes.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgTablesSizesDefault defines default query for getting stats related to tables' sizes. 5 | PgTablesSizesDefault = "SELECT s.schemaname ||'.'|| s.relname AS relation," + 6 | "(SELECT count(*) FROM pg_index i WHERE i.indrelid = s.relid) AS indexes_total," + 7 | `pg_total_relation_size(s.relid) / 1024 AS "size_total,KiB",` + 8 | `pg_relation_size(s.relid, 'main') / 1024 AS "table_total,KiB",` + 9 | `(pg_relation_size(s.relid, 'fsm') + pg_relation_size(s.relid, 'vm') + pg_relation_size(s.relid, 'init')) / 1024 AS "meta_total,KiB",` + 10 | `coalesce(pg_relation_size((SELECT reltoastrelid FROM pg_class c WHERE c.oid = s.relid )) / 1024, 0) AS "toast_total,KiB",` + 11 | `pg_indexes_size(s.relid) / 1024 AS "indexes_total,KiB",` + 12 | `pg_total_relation_size(s.relid) / 1024 AS "size,KiB",` + 13 | `pg_relation_size(s.relid, 'main') / 1024 AS "table,KiB",` + 14 | `(pg_relation_size(s.relid, 'fsm') + pg_relation_size(s.relid, 'vm') + pg_relation_size(s.relid, 'init')) / 1024 AS "meta,KiB",` + 15 | `coalesce(pg_relation_size((SELECT reltoastrelid FROM pg_class c WHERE c.oid = s.relid )) / 1024, 0) AS "toast,KiB",` + 16 | `pg_indexes_size(s.relid) / 1024 AS "indexes,KiB" ` + 17 | "FROM pg_stat_{{.ViewType}}_tables s, pg_class c " + 18 | "WHERE s.relid = c.oid AND NOT EXISTS (SELECT 1 FROM pg_locks WHERE relation = s.relid AND mode = 'AccessExclusiveLock' AND granted) " + 19 | "ORDER BY (s.schemaname || '.' || s.relname) DESC" 20 | ) 21 | -------------------------------------------------------------------------------- /internal/query/sizes_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_StatSizesQueries(t *testing.T) { 11 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 12 | 13 | for _, version := range versions { 14 | t.Run(fmt.Sprintf("sizes/%d", version), func(t *testing.T) { 15 | tmpl := PgTablesSizesDefault 16 | 17 | opts := NewOptions(version, "f", "off", 256, "public") 18 | q, err := Format(tmpl, opts) 19 | assert.NoError(t, err) 20 | 21 | conn, err := postgres.NewTestConnectVersion(version) 22 | assert.NoError(t, err) 23 | 24 | _, err = conn.Exec(q) 25 | assert.NoError(t, err) 26 | 27 | conn.Close() 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/query/statements_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestSelectStatStatementsTimingQuery(t *testing.T) { 11 | testcases := []struct { 12 | version int 13 | want string 14 | }{ 15 | {version: 90500, want: PgStatStatementsTimingPG12}, 16 | {version: 90600, want: PgStatStatementsTimingPG12}, 17 | {version: 100000, want: PgStatStatementsTimingPG12}, 18 | {version: 110000, want: PgStatStatementsTimingPG12}, 19 | {version: 120000, want: PgStatStatementsTimingPG12}, 20 | {version: 130000, want: PgStatStatementsTimingDefault}, 21 | } 22 | 23 | for _, tc := range testcases { 24 | got := SelectStatStatementsTimingQuery(tc.version) 25 | assert.Equal(t, tc.want, got) 26 | } 27 | } 28 | 29 | func Test_StatStatementsQueries(t *testing.T) { 30 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 31 | 32 | queries := []string{ 33 | PgStatStatementsGeneralDefault, 34 | PgStatStatementsIoDefault, 35 | PgStatStatementsTempDefault, 36 | PgStatStatementsLocalDefault, 37 | } 38 | 39 | for _, version := range versions { 40 | t.Run(fmt.Sprintf("pg_stat_statements/%d", version), func(t *testing.T) { 41 | for _, query := range queries { 42 | opts := NewOptions(version, "f", "off", 256, "public") 43 | q, err := Format(query, opts) 44 | assert.NoError(t, err) 45 | 46 | conn, err := postgres.NewTestConnectVersion(version) 47 | assert.NoError(t, err) 48 | 49 | _, err = conn.Exec(q) 50 | assert.NoError(t, err) 51 | 52 | conn.Close() 53 | } 54 | }) 55 | } 56 | 57 | t.Run("pg_stat_statements_timing", func(t *testing.T) { 58 | for _, version := range versions { 59 | tmpl := SelectStatStatementsTimingQuery(version) 60 | opts := NewOptions(version, "f", "off", 256, "public") 61 | q, err := Format(tmpl, opts) 62 | assert.NoError(t, err) 63 | 64 | conn, err := postgres.NewTestConnectVersion(version) 65 | assert.NoError(t, err) 66 | 67 | _, err = conn.Exec(q) 68 | assert.NoError(t, err) 69 | 70 | conn.Close() 71 | } 72 | }) 73 | 74 | t.Run("pg_stat_statements_wal", func(t *testing.T) { 75 | for _, version := range []int{130000} { 76 | tmpl := PgStatStatementsWalDefault 77 | opts := NewOptions(version, "f", "off", 256, "public") 78 | q, err := Format(tmpl, opts) 79 | assert.NoError(t, err) 80 | 81 | conn, err := postgres.NewTestConnectVersion(version) 82 | assert.NoError(t, err) 83 | 84 | _, err = conn.Exec(q) 85 | assert.NoError(t, err) 86 | 87 | conn.Close() 88 | } 89 | }) 90 | } 91 | 92 | func TestSelectQueryReportQuery(t *testing.T) { 93 | testcases := []struct { 94 | version int 95 | want string 96 | }{ 97 | {version: 90500, want: PgStatStatementsReportQueryPG12}, 98 | {version: 90600, want: PgStatStatementsReportQueryPG12}, 99 | {version: 100000, want: PgStatStatementsReportQueryPG12}, 100 | {version: 110000, want: PgStatStatementsReportQueryPG12}, 101 | {version: 120000, want: PgStatStatementsReportQueryPG12}, 102 | {version: 130000, want: PgStatStatementsReportQueryDefault}, 103 | } 104 | 105 | for _, tc := range testcases { 106 | got := SelectQueryReportQuery(tc.version) 107 | assert.Equal(t, tc.want, got) 108 | } 109 | } 110 | 111 | func Test_StatStatementsReportQueries(t *testing.T) { 112 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 113 | 114 | for _, version := range versions { 115 | tmpl := SelectQueryReportQuery(version) 116 | opts := NewOptions(version, "f", "off", 256, "public") 117 | q, err := Format(tmpl, opts) 118 | assert.NoError(t, err) 119 | 120 | conn, err := postgres.NewTestConnectVersion(version) 121 | assert.NoError(t, err) 122 | 123 | // Use fake query_id, just test queries are executed with no errors. 124 | _, err = conn.Exec(q, "1234567890") 125 | assert.NoError(t, err) 126 | 127 | conn.Close() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /internal/query/tables.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatTablesDefault defines default query for getting tables' stats from pg_stat_all_tables and pg_statio_all_tables views. 5 | PgStatTablesDefault = "SELECT t.schemaname || '.' || t.relname AS relation, " + 6 | "coalesce(t.seq_scan, 0) AS seq_scan, coalesce(t.seq_tup_read, 0) AS seq_read, " + 7 | "coalesce(t.idx_scan, 0) AS idx_scan, coalesce(t.idx_tup_fetch, 0) AS idx_fetch, " + 8 | "coalesce(t.n_tup_ins, 0) AS inserts, coalesce(t.n_tup_upd, 0) AS updates, " + 9 | "coalesce(t.n_tup_del, 0) AS deletes, coalesce(t.n_tup_hot_upd, 0) AS hot_updates, " + 10 | "coalesce(t.n_live_tup, 0) AS live, coalesce(t.n_dead_tup, 0) AS dead, " + 11 | `coalesce(i.heap_blks_read * (SELECT current_setting('block_size')::int / 1024), 0) AS "heap_read,KiB", ` + 12 | "coalesce(i.heap_blks_hit, 0) AS heap_hit, " + 13 | `coalesce(i.idx_blks_read * (SELECT current_setting('block_size')::int / 1024), 0) AS "idx_read,KiB", ` + 14 | "coalesce(i.idx_blks_hit, 0) AS idx_hit, " + 15 | `coalesce(i.toast_blks_read * (SELECT current_setting('block_size')::int / 1024), 0) AS "toast_read,KiB", ` + 16 | "coalesce(i.toast_blks_hit, 0) AS toast_hit, " + 17 | `coalesce(i.tidx_blks_read * (SELECT current_setting('block_size')::int / 1024), 0) AS "tidx_read,KiB", ` + 18 | "coalesce(i.tidx_blks_hit, 0) AS tidx_hit " + 19 | "FROM pg_stat_{{.ViewType}}_tables t, pg_statio_{{.ViewType}}_tables i " + 20 | "WHERE t.relid = i.relid ORDER BY (t.schemaname || '.' || t.relname) DESC" 21 | ) 22 | -------------------------------------------------------------------------------- /internal/query/tables_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_StatTablesQueries(t *testing.T) { 11 | versions := []int{90500, 90600, 100000, 110000, 120000, 130000, 140000} 12 | 13 | for _, version := range versions { 14 | t.Run(fmt.Sprintf("pg_stat_tables/%d", version), func(t *testing.T) { 15 | tmpl := PgStatTablesDefault 16 | 17 | opts := NewOptions(version, "f", "off", 256, "public") 18 | q, err := Format(tmpl, opts) 19 | assert.NoError(t, err) 20 | 21 | conn, err := postgres.NewTestConnectVersion(version) 22 | assert.NoError(t, err) 23 | 24 | _, err = conn.Exec(q) 25 | assert.NoError(t, err) 26 | 27 | conn.Close() 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/query/wal.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | const ( 4 | // PgStatWALDefault defines default query for getting WAL stats from pg_stat_wal. 5 | PgStatWALDefault = "SELECT 'WAL' AS source, " + 6 | "(SELECT pg_size_pretty(count(1) * pg_size_bytes(current_setting('wal_segment_size'))) AS waldir_size FROM pg_ls_waldir()) AS waldir_size, " + 7 | `round(wal_bytes / 1024, 2) AS "wal,KiB", ` + 8 | "wal_records AS records, wal_fpi AS fpi, " + 9 | `wal_write AS write, wal_sync AS sync, wal_write_time AS "write,ms", wal_sync_time AS "sync,ms", wal_buffers_full AS buffers_full, ` + 10 | "date_trunc('seconds', now() - stats_reset)::text AS stats_age " + 11 | "FROM pg_stat_wal" 12 | ) 13 | -------------------------------------------------------------------------------- /internal/query/wal_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | // Test_StatWALQueries tests query, executing it against all supported Postgres versions. 11 | func Test_StatWALQueries(t *testing.T) { 12 | versions := []int{140000} 13 | 14 | for _, version := range versions { 15 | t.Run(fmt.Sprintf("pg_stat_wal/%d", version), func(t *testing.T) { 16 | tmpl := PgStatWALDefault 17 | 18 | opts := NewOptions(version, "f", "off", 256, "public") 19 | q, err := Format(tmpl, opts) 20 | assert.NoError(t, err) 21 | 22 | conn, err := postgres.NewTestConnectVersion(version) 23 | assert.NoError(t, err) 24 | 25 | _, err = conn.Exec(q) 26 | assert.NoError(t, err) 27 | 28 | conn.Close() 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/stat/cpu.go: -------------------------------------------------------------------------------- 1 | // Stuff related to CPU usage stats 2 | 3 | package stat 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "github.com/lesovsky/pgcenter/internal/postgres" 9 | "io" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | // CpuStat describes CPU statistics based on /proc/stat. 16 | type CpuStat struct { 17 | Entry string 18 | User float64 19 | Nice float64 20 | Sys float64 21 | Idle float64 22 | Iowait float64 23 | Irq float64 24 | Softirq float64 25 | Steal float64 26 | Guest float64 27 | GstNice float64 28 | Total float64 29 | } 30 | 31 | // readCpuStat returns CPU stats based on type of passed DB connection. 32 | func readCpuStat(db *postgres.DB, schemaExists bool) (CpuStat, error) { 33 | if db.Local { 34 | return readCpuStatLocal("/proc/stat") 35 | } else if schemaExists { 36 | return readCpuStatRemote(db) 37 | } 38 | 39 | return CpuStat{}, nil 40 | } 41 | 42 | // readCpuStatLocal returns CPU stats read from local proc file. 43 | func readCpuStatLocal(statfile string) (CpuStat, error) { 44 | var stat CpuStat 45 | f, err := os.Open(filepath.Clean(statfile)) 46 | if err != nil { 47 | return stat, err 48 | } 49 | defer func() { 50 | _ = f.Close() 51 | }() 52 | 53 | scanner := bufio.NewScanner(f) 54 | 55 | for scanner.Scan() { 56 | line := scanner.Text() 57 | 58 | parts := strings.Fields(line) 59 | if len(parts) < 2 { 60 | continue 61 | } 62 | 63 | // Looking only for total stat, skip per-CPU stats. 64 | if parts[0] != "cpu" { 65 | continue 66 | } 67 | 68 | count, err := fmt.Sscanf( 69 | line, 70 | "%s %f %f %f %f %f %f %f %f %f %f", 71 | &stat.Entry, &stat.User, &stat.Nice, &stat.Sys, &stat.Idle, &stat.Iowait, &stat.Irq, &stat.Softirq, &stat.Steal, &stat.Guest, &stat.GstNice, 72 | ) 73 | 74 | if err != nil && err != io.EOF { 75 | return stat, fmt.Errorf("%s bad content: %s", statfile, err) 76 | } 77 | if count != 11 { 78 | return stat, fmt.Errorf("%s bad content: not enough fields in '%s'", statfile, line) 79 | } 80 | 81 | stat.Total = stat.User + stat.Nice + stat.Sys + stat.Idle + stat.Iowait + stat.Irq + stat.Softirq + stat.Steal + stat.Guest 82 | 83 | // No reason to read next lines. 84 | break 85 | } 86 | 87 | return stat, scanner.Err() 88 | } 89 | 90 | // readCpuStatRemote returns CPU stats from SQL stats schema. 91 | func readCpuStatRemote(db *postgres.DB) (CpuStat, error) { 92 | var stat CpuStat 93 | q := `SELECT cpu,us_time::numeric,ni_time::numeric,sy_time::numeric,id_time::numeric,wa_time::numeric,hi_time::numeric,si_time::numeric,st_time::numeric,quest_time::numeric,guest_ni_time::numeric FROM pgcenter.sys_proc_stat WHERE cpu = 'cpu'` 94 | err := db.QueryRow(q).Scan(&stat.Entry, &stat.User, &stat.Nice, &stat.Sys, &stat.Idle, 95 | &stat.Iowait, &stat.Irq, &stat.Softirq, &stat.Steal, &stat.Guest, &stat.GstNice) 96 | if err != nil { 97 | return stat, err 98 | } 99 | 100 | stat.Total = stat.User + stat.Nice + stat.Sys + stat.Idle + stat.Iowait + stat.Irq + stat.Softirq + stat.Steal + stat.Guest 101 | 102 | return stat, nil 103 | } 104 | 105 | // countCpuUsage compares CPU stats snapshots and returns CPU usage stats over time interval. 106 | func countCpuUsage(prev CpuStat, curr CpuStat, ticks float64) CpuStat { 107 | var stat CpuStat 108 | itv := curr.Total - prev.Total 109 | 110 | stat.User = sValue(prev.User, curr.User, itv, ticks) 111 | stat.Nice = sValue(prev.Nice, curr.Nice, itv, ticks) 112 | stat.Sys = sValue(prev.Sys, curr.Sys, itv, ticks) 113 | stat.Idle = sValue(prev.Idle, curr.Idle, itv, ticks) 114 | stat.Iowait = sValue(prev.Iowait, curr.Iowait, itv, ticks) 115 | stat.Irq = sValue(prev.Irq, curr.Irq, itv, ticks) 116 | stat.Softirq = sValue(prev.Softirq, curr.Softirq, itv, ticks) 117 | stat.Steal = sValue(prev.Steal, curr.Steal, itv, ticks) 118 | 119 | return stat 120 | } 121 | -------------------------------------------------------------------------------- /internal/stat/cpu_test.go: -------------------------------------------------------------------------------- 1 | package stat 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_readCpuStat(t *testing.T) { 10 | conn, err := postgres.NewTestConnect() 11 | assert.NoError(t, err) 12 | defer conn.Close() 13 | 14 | // test "local" reading 15 | conn.Local = true 16 | got, err := readCpuStat(conn, false) 17 | assert.NoError(t, err) 18 | assert.Greater(t, got.Total, float64(0)) 19 | 20 | // test "remote" reading 21 | conn.Local = false 22 | got, err = readCpuStat(conn, true) 23 | assert.NoError(t, err) 24 | assert.Greater(t, got.Total, float64(0)) 25 | 26 | // test "remote", but when schema is not available 27 | got, err = readCpuStat(conn, false) 28 | assert.NoError(t, err) 29 | assert.Equal(t, got.Total, float64(0)) 30 | } 31 | 32 | func Test_readCpuStatLocal(t *testing.T) { 33 | testcases := []struct { 34 | statfile string 35 | valid bool 36 | want CpuStat 37 | }{ 38 | { 39 | statfile: "testdata/proc/stat.golden", 40 | valid: true, 41 | want: CpuStat{ 42 | Entry: "cpu", 43 | User: 3097668, 44 | Nice: 1593, 45 | Sys: 1419618, 46 | Idle: 132242258, 47 | Iowait: 42535, 48 | Irq: 0, 49 | Softirq: 384686, 50 | Steal: 0, 51 | Guest: 0, 52 | GstNice: 0, 53 | Total: 137188358, 54 | }, 55 | }, 56 | {statfile: "testdata/proc/stat.unknown", valid: false}, 57 | } 58 | 59 | for _, tc := range testcases { 60 | got, err := readCpuStatLocal(tc.statfile) 61 | if tc.valid { 62 | assert.NoError(t, err) 63 | assert.Equal(t, tc.want, got) 64 | } else { 65 | assert.Error(t, err) 66 | } 67 | } 68 | } 69 | 70 | func Test_readCpuStatRemote(t *testing.T) { 71 | conn, err := postgres.NewTestConnect() 72 | assert.NoError(t, err) 73 | 74 | got, err := readCpuStatRemote(conn) 75 | assert.NoError(t, err) 76 | assert.Greater(t, got.Total, float64(0)) 77 | assert.Greater(t, got.User, float64(0)) 78 | assert.Greater(t, got.Sys, float64(0)) 79 | 80 | conn.Close() 81 | _, err = readCpuStatRemote(conn) 82 | assert.Error(t, err) 83 | } 84 | 85 | func Test_countCpuUsage(t *testing.T) { 86 | prev, err := readCpuStatLocal("testdata/proc/stat.golden") 87 | assert.NoError(t, err) 88 | 89 | curr, err := readCpuStatLocal("testdata/proc/stat2.golden") 90 | assert.NoError(t, err) 91 | 92 | got := countCpuUsage(prev, curr, 100) 93 | 94 | want := CpuStat{ 95 | Entry: "", 96 | User: 16.666666666666664, 97 | Nice: 16.666666666666664, 98 | Sys: 16.666666666666664, 99 | Idle: 16.666666666666664, 100 | Iowait: 16.666666666666664, 101 | Irq: 0, 102 | Softirq: 16.666666666666664, 103 | Steal: 0, 104 | Guest: 0, 105 | GstNice: 0, 106 | Total: 0, 107 | } 108 | 109 | assert.Equal(t, want, got) 110 | } 111 | -------------------------------------------------------------------------------- /internal/stat/fsstat_test.go: -------------------------------------------------------------------------------- 1 | package stat 2 | 3 | import ( 4 | "context" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/stretchr/testify/assert" 7 | "os" 8 | "path/filepath" 9 | "sync" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func Test_parseProcMounts(t *testing.T) { 15 | file, err := os.Open(filepath.Clean("testdata/proc/mounts.golden")) 16 | assert.NoError(t, err) 17 | defer func() { _ = file.Close() }() 18 | 19 | stats, err := parseProcMounts(file) 20 | assert.NoError(t, err) 21 | 22 | want := []Mount{ 23 | {Device: "/dev/mapper/ssd-root", Mountpoint: "/", Fstype: "ext4", Options: "rw,relatime,discard,errors=remount-ro"}, 24 | {Device: "/dev/sda1", Mountpoint: "/boot", Fstype: "ext3", Options: "rw,relatime"}, 25 | {Device: "/dev/mapper/ssd-data", Mountpoint: "/data", Fstype: "ext4", Options: "rw,relatime,discard"}, 26 | {Device: "/dev/sdc1", Mountpoint: "/archive", Fstype: "xfs", Options: "rw,relatime"}, 27 | } 28 | 29 | assert.Equal(t, want, stats) 30 | 31 | // test with wrong format file 32 | file, err = os.Open(filepath.Clean("testdata/proc/netdev.v1.golden")) 33 | assert.NoError(t, err) 34 | defer func() { _ = file.Close() }() 35 | 36 | stats, err = parseProcMounts(file) 37 | assert.Error(t, err) 38 | assert.Nil(t, stats) 39 | } 40 | 41 | func Test_readFilesystemStatsLocal(t *testing.T) { 42 | got, err := readFilesystemStatsLocal("/proc/mounts") 43 | assert.NoError(t, err) 44 | assert.NotNil(t, got) 45 | assert.Greater(t, len(got), 0) 46 | } 47 | 48 | func Test_parseFilesystemStats(t *testing.T) { 49 | file, err := os.Open(filepath.Clean("testdata/proc/mounts.golden")) 50 | assert.NoError(t, err) 51 | 52 | stats, err := parseFilesystemStats(file) 53 | assert.NoError(t, err) 54 | assert.Greater(t, len(stats), 1) 55 | assert.Greater(t, stats[0].Size, float64(0)) 56 | assert.Greater(t, stats[0].Free, float64(0)) 57 | assert.Greater(t, stats[0].Avail, float64(0)) 58 | assert.Greater(t, stats[0].Used, float64(0)) 59 | assert.Greater(t, stats[0].Reserved, float64(0)) 60 | assert.Greater(t, stats[0].Pused, float64(0)) 61 | assert.Greater(t, stats[0].Files, float64(0)) 62 | assert.Greater(t, stats[0].Filesfree, float64(0)) 63 | assert.Greater(t, stats[0].Filesused, float64(0)) 64 | assert.Greater(t, stats[0].Filespused, float64(0)) 65 | 66 | _ = file.Close() 67 | 68 | // test with wrong format file 69 | file, err = os.Open(filepath.Clean("testdata/proc/netdev.v1.golden")) 70 | assert.NoError(t, err) 71 | 72 | stats, err = parseFilesystemStats(file) 73 | assert.Error(t, err) 74 | assert.Nil(t, stats) 75 | _ = file.Close() 76 | } 77 | 78 | func Test_readMountpointStat(t *testing.T) { 79 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 80 | defer cancel() 81 | 82 | wg := sync.WaitGroup{} 83 | ch := make(chan Fsstat) 84 | 85 | wg.Add(1) 86 | go readMountpointStat("/", ch, &wg) 87 | 88 | select { 89 | case response := <-ch: 90 | assert.Greater(t, response.Size, float64(0)) 91 | assert.Greater(t, response.Free, float64(0)) 92 | assert.Greater(t, response.Avail, float64(0)) 93 | assert.Greater(t, response.Used, float64(0)) 94 | assert.Greater(t, response.Reserved, float64(0)) 95 | assert.Greater(t, response.Pused, float64(0)) 96 | assert.Greater(t, response.Files, float64(0)) 97 | assert.Greater(t, response.Filesfree, float64(0)) 98 | assert.Greater(t, response.Filesused, float64(0)) 99 | assert.Greater(t, response.Filespused, float64(0)) 100 | case <-ctx.Done(): 101 | assert.Fail(t, "context cancelled: ", ctx.Err()) 102 | } 103 | 104 | wg.Wait() 105 | close(ch) 106 | } 107 | 108 | func Test_readFilesystemStatsRemote(t *testing.T) { 109 | conn, err := postgres.NewTestConnect() 110 | assert.NoError(t, err) 111 | 112 | got, err := readFilesystemStatsRemote(conn) 113 | assert.NoError(t, err) 114 | assert.Greater(t, len(got), 0) 115 | 116 | // Check device value is not empty 117 | for i := range got { 118 | assert.NotEqual(t, got[i].Mount.Device, "") 119 | } 120 | 121 | conn.Close() 122 | _, err = readFilesystemStatsRemote(conn) 123 | assert.Error(t, err) 124 | } 125 | -------------------------------------------------------------------------------- /internal/stat/loadavg.go: -------------------------------------------------------------------------------- 1 | // Stuff related to 'load average' stats 2 | 3 | package stat 4 | 5 | import ( 6 | "fmt" 7 | "github.com/lesovsky/pgcenter/internal/postgres" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // LoadAvg describes 'load average' stats based on /proc/loadavg. 15 | type LoadAvg struct { 16 | One float64 17 | Five float64 18 | Fifteen float64 19 | } 20 | 21 | // readLoadAverage returns load average stats based on type of passed DB connection. 22 | func readLoadAverage(db *postgres.DB, schemaExists bool) (LoadAvg, error) { 23 | if db.Local { 24 | return readLoadAverageLocal("/proc/loadavg") 25 | } else if schemaExists { 26 | return readLoadAverageRemote(db) 27 | } 28 | 29 | return LoadAvg{}, nil 30 | } 31 | 32 | // readLoadAverageLocal returns load average stats read from local proc file. 33 | func readLoadAverageLocal(statfile string) (LoadAvg, error) { 34 | var stat LoadAvg 35 | 36 | data, err := os.ReadFile(filepath.Clean(statfile)) 37 | if err != nil { 38 | return stat, err 39 | } 40 | 41 | fields := strings.Fields(string(data)) 42 | 43 | if len(fields) < 3 { 44 | return stat, fmt.Errorf("%s invalid content", statfile) 45 | } 46 | 47 | values := make([]float64, 3) 48 | for i, value := range fields[0:3] { 49 | values[i], err = strconv.ParseFloat(value, 64) 50 | if err != nil { 51 | return stat, err 52 | } 53 | } 54 | 55 | stat.One, stat.Five, stat.Fifteen = values[0], values[1], values[2] 56 | 57 | return stat, nil 58 | } 59 | 60 | // readLoadAverageRemote returns load average stats from SQL stats schema. 61 | func readLoadAverageRemote(db *postgres.DB) (LoadAvg, error) { 62 | var stat LoadAvg 63 | err := db.QueryRow("SELECT min1, min5, min15 FROM pgcenter.sys_proc_loadavg").Scan(&stat.One, &stat.Five, &stat.Fifteen) 64 | if err != nil { 65 | return stat, err 66 | } 67 | return stat, nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/stat/loadavg_test.go: -------------------------------------------------------------------------------- 1 | package stat 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_readLoadAverage(t *testing.T) { 10 | conn, err := postgres.NewTestConnect() 11 | assert.NoError(t, err) 12 | defer conn.Close() 13 | 14 | // test "local" reading 15 | conn.Local = true 16 | got, err := readLoadAverage(conn, false) 17 | assert.NoError(t, err) 18 | assert.Greater(t, got.One, float64(0)) 19 | 20 | // test "remote" reading 21 | conn.Local = false 22 | got, err = readLoadAverage(conn, true) 23 | assert.NoError(t, err) 24 | assert.Greater(t, got.One, float64(0)) 25 | 26 | // test "remote", but when schema is not available 27 | got, err = readLoadAverage(conn, false) 28 | assert.NoError(t, err) 29 | assert.Equal(t, got.One, float64(0)) 30 | } 31 | 32 | func Test_readLoadAverageLocal(t *testing.T) { 33 | testcases := []struct { 34 | statfile string 35 | valid bool 36 | want LoadAvg 37 | }{ 38 | {statfile: "testdata/proc/loadavg.golden", valid: true, want: LoadAvg{One: 2.43, Five: 2.30, Fifteen: 1.74}}, 39 | {statfile: "testdata/proc/loadavg.invalid", valid: false}, 40 | } 41 | 42 | for _, tc := range testcases { 43 | got, err := readLoadAverageLocal(tc.statfile) 44 | if tc.valid { 45 | assert.NoError(t, err) 46 | assert.Equal(t, tc.want, got) 47 | } else { 48 | assert.Error(t, err) 49 | } 50 | } 51 | } 52 | 53 | func Test_readLoadAverageRemote(t *testing.T) { 54 | conn, err := postgres.NewTestConnect() 55 | assert.NoError(t, err) 56 | 57 | got, err := readLoadAverageRemote(conn) 58 | assert.NoError(t, err) 59 | assert.Greater(t, got.One, float64(0)) 60 | assert.Greater(t, got.Five, float64(0)) 61 | assert.Greater(t, got.Fifteen, float64(0)) 62 | 63 | conn.Close() 64 | _, err = readLoadAverageRemote(conn) 65 | assert.Error(t, err) 66 | } 67 | -------------------------------------------------------------------------------- /internal/stat/log_test.go: -------------------------------------------------------------------------------- 1 | package stat 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/stretchr/testify/assert" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestLogfile(t *testing.T) { 11 | l := Logfile{Path: "./testdata/log/postgresql.log"} 12 | 13 | // Test log opening 14 | assert.NoError(t, l.Open()) 15 | assert.NotNil(t, l.File) 16 | 17 | // Test reading 18 | buf, err := l.Read(18, 3000) 19 | assert.NoError(t, err) 20 | assert.NotNil(t, buf) 21 | assert.Contains(t, string(buf), "2020-12-05 00:03:46 +05 [1361]: [6517-1] LOG:") 22 | assert.Contains(t, string(buf), "2020-12-05 10:01:20 +05 [1361]: [6756-1] LOG:") 23 | 24 | // Test close 25 | assert.NoError(t, l.Close()) 26 | 27 | // Test opening unknown log 28 | l = Logfile{Path: "./testdata/log/invalid.log"} 29 | assert.Error(t, l.Open()) 30 | } 31 | 32 | func TestGetPostgresCurrentLogfile(t *testing.T) { 33 | conn, err := postgres.NewTestConnect() 34 | assert.NoError(t, err) 35 | defer conn.Close() 36 | 37 | // Test PG96 and older 38 | logfile, err := GetPostgresCurrentLogfile(conn, 96000) 39 | assert.NoError(t, err) 40 | assert.NotEqual(t, "", logfile) 41 | 42 | // Test PG10 and newer 43 | logfile, err = GetPostgresCurrentLogfile(conn, 100000) 44 | assert.NoError(t, err) 45 | assert.NotEqual(t, "", logfile) 46 | } 47 | 48 | func Test_lookupPostgresLogfile(t *testing.T) { 49 | conn, err := postgres.NewTestConnect() 50 | assert.NoError(t, err) 51 | defer conn.Close() 52 | 53 | s, err := lookupPostgresLogfile(conn) 54 | assert.NoError(t, err) 55 | assert.NotEqual(t, "", s) 56 | } 57 | 58 | func Test_assemblePostgresLogfile(t *testing.T) { 59 | _, err := os.Create("/tmp/test-000000.log") 60 | assert.NoError(t, err) 61 | _, err = os.Create("/tmp/test-123456.log") 62 | assert.NoError(t, err) 63 | 64 | defer func() { 65 | assert.NoError(t, os.Remove("/tmp/test-000000.log")) 66 | assert.NoError(t, os.Remove("/tmp/test-123456.log")) 67 | }() 68 | 69 | testcases := []struct { 70 | datadir string 71 | logdir string 72 | logfilename string 73 | startTime string 74 | timezone string 75 | want string 76 | }{ 77 | { 78 | datadir: "/tmp", logdir: "/var/log/postgresql", logfilename: "postgresql.log", 79 | want: "/var/log/postgresql/postgresql.log", 80 | }, 81 | { 82 | datadir: "/tmp", logdir: "pg_log", logfilename: "postgresql.log", 83 | want: "/tmp/pg_log/postgresql.log", 84 | }, 85 | { 86 | datadir: "/tmp", logdir: "/tmp", logfilename: "test-%H%M%S.log", 87 | startTime: "123456", 88 | want: "/tmp/test-123456.log", 89 | }, 90 | { 91 | datadir: "/tmp", logdir: "/tmp", logfilename: "test-%H%M%S.log", 92 | startTime: "111111", 93 | want: "/tmp/test-000000.log", 94 | }, 95 | { 96 | // case with log specified but which is not exists 97 | datadir: "/tmp", logdir: "/tmp", logfilename: "test-%y%m%d.log", 98 | want: "", 99 | }, 100 | } 101 | 102 | for _, tc := range testcases { 103 | got := assemblePostgresLogfile(tc.datadir, tc.logdir, tc.logfilename, tc.startTime, tc.timezone) 104 | assert.Equal(t, tc.want, got) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /internal/stat/memstat.go: -------------------------------------------------------------------------------- 1 | // Stuff related to memory/swap usage stats 2 | 3 | package stat 4 | 5 | import ( 6 | "bufio" 7 | "github.com/lesovsky/pgcenter/internal/postgres" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // Meminfo describes memory/swap stats based on /proc/meminfo. 15 | type Meminfo struct { 16 | MemTotal uint64 17 | MemFree uint64 18 | MemUsed uint64 19 | SwapTotal uint64 20 | SwapFree uint64 21 | SwapUsed uint64 22 | MemCached uint64 23 | MemBuffers uint64 24 | MemDirty uint64 25 | MemWriteback uint64 26 | MemSlab uint64 27 | } 28 | 29 | // readMeminfo returns memory/swap stats based on type of passed DB connection. 30 | func readMeminfo(db *postgres.DB, schemaExists bool) (Meminfo, error) { 31 | if db.Local { 32 | return readMeminfoLocal("/proc/meminfo") 33 | } else if schemaExists { 34 | return readMeminfoRemote(db) 35 | } 36 | 37 | return Meminfo{}, nil 38 | } 39 | 40 | // readMeminfoLocal returns memory/swap stats read from local proc file. 41 | func readMeminfoLocal(statfile string) (Meminfo, error) { 42 | var stat Meminfo 43 | 44 | f, err := os.Open(filepath.Clean(statfile)) 45 | if err != nil { 46 | return stat, err 47 | } 48 | defer func() { 49 | _ = f.Close() 50 | }() 51 | 52 | scanner := bufio.NewScanner(f) 53 | 54 | for scanner.Scan() { 55 | line := scanner.Text() 56 | 57 | fields := strings.Fields(line) 58 | if len(fields) < 3 { 59 | // TODO: log error to stderr 60 | continue 61 | } 62 | 63 | value, err := strconv.ParseUint(fields[1], 10, 64) 64 | if err != nil { 65 | // TODO: log error to stderr 66 | continue 67 | } 68 | 69 | switch fields[0] { 70 | case "MemTotal:": 71 | stat.MemTotal = value / 1024 72 | case "MemFree:": 73 | stat.MemFree = value / 1024 74 | case "SwapTotal:": 75 | stat.SwapTotal = value / 1024 76 | case "SwapFree:": 77 | stat.SwapFree = value / 1024 78 | case "Cached:": 79 | stat.MemCached = value / 1024 80 | case "Dirty:": 81 | stat.MemDirty = value / 1024 82 | case "Writeback:": 83 | stat.MemWriteback = value / 1024 84 | case "Buffers:": 85 | stat.MemBuffers = value / 1024 86 | case "Slab:": 87 | stat.MemSlab = value / 1024 88 | } 89 | } 90 | stat.MemUsed = stat.MemTotal - stat.MemFree - stat.MemCached - stat.MemBuffers - stat.MemSlab 91 | stat.SwapUsed = stat.SwapTotal - stat.SwapFree 92 | 93 | return stat, scanner.Err() 94 | } 95 | 96 | // readMeminfoRemote returns memory/swap stats from SQL stats schema. 97 | func readMeminfoRemote(db *postgres.DB) (Meminfo, error) { 98 | var stat Meminfo 99 | 100 | query := `SELECT metric, metric_value 101 | FROM pgcenter.sys_proc_meminfo 102 | WHERE metric IN ('MemTotal:','MemFree:','SwapTotal:','SwapFree:', 'Cached:','Dirty:','Writeback:','Buffers:','Slab:') 103 | ORDER BY 1` 104 | 105 | rows, err := db.Query(query) 106 | if err != nil { 107 | return stat, err 108 | } 109 | defer rows.Close() 110 | 111 | var name string 112 | var value uint64 113 | for rows.Next() { 114 | if err := rows.Scan(&name, &value); err != nil { 115 | // TODO: log error to stderr 116 | continue 117 | } 118 | 119 | switch name { 120 | case "MemTotal:": 121 | stat.MemTotal = value / 1024 122 | case "MemFree:": 123 | stat.MemFree = value / 1024 124 | case "SwapTotal:": 125 | stat.SwapTotal = value / 1024 126 | case "SwapFree:": 127 | stat.SwapFree = value / 1024 128 | case "Cached:": 129 | stat.MemCached = value / 1024 130 | case "Dirty:": 131 | stat.MemDirty = value / 1024 132 | case "Writeback:": 133 | stat.MemWriteback = value / 1024 134 | case "Buffers:": 135 | stat.MemBuffers = value / 1024 136 | case "Slab:": 137 | stat.MemSlab = value / 1024 138 | } 139 | } 140 | 141 | stat.MemUsed = stat.MemTotal - stat.MemFree - stat.MemCached - stat.MemBuffers - stat.MemSlab 142 | stat.SwapUsed = stat.SwapTotal - stat.SwapFree 143 | 144 | return stat, rows.Err() 145 | } 146 | -------------------------------------------------------------------------------- /internal/stat/memstat_test.go: -------------------------------------------------------------------------------- 1 | package stat 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_readMeminfo(t *testing.T) { 10 | conn, err := postgres.NewTestConnect() 11 | assert.NoError(t, err) 12 | defer conn.Close() 13 | 14 | // test "local" reading 15 | conn.Local = true 16 | got, err := readMeminfo(conn, false) 17 | assert.NoError(t, err) 18 | assert.Greater(t, got.MemTotal, uint64(0)) 19 | 20 | // test "remote" reading 21 | conn.Local = false 22 | got, err = readMeminfo(conn, true) 23 | assert.NoError(t, err) 24 | assert.Greater(t, got.MemTotal, uint64(0)) 25 | 26 | // test "remote", but when schema is not available 27 | got, err = readMeminfo(conn, false) 28 | assert.NoError(t, err) 29 | assert.Equal(t, got.MemTotal, uint64(0)) 30 | } 31 | 32 | func Test_readMeminfoLocal(t *testing.T) { 33 | testcases := []struct { 34 | statfile string 35 | valid bool 36 | want Meminfo 37 | }{ 38 | {statfile: "testdata/proc/meminfo.golden", valid: true, want: Meminfo{ 39 | MemTotal: 32069, MemFree: 21064, MemUsed: 5481, 40 | SwapTotal: 16383, SwapFree: 16383, SwapUsed: 0, 41 | MemCached: 4259, MemBuffers: 589, MemDirty: 35, MemWriteback: 0, MemSlab: 676, 42 | }}, 43 | {statfile: "testdata/proc/meminfo.unknown", valid: false}, 44 | } 45 | 46 | for _, tc := range testcases { 47 | got, err := readMeminfoLocal(tc.statfile) 48 | if tc.valid { 49 | assert.NoError(t, err) 50 | assert.Equal(t, tc.want, got) 51 | } else { 52 | assert.Error(t, err) 53 | } 54 | } 55 | } 56 | 57 | func Test_readMeminfoRemote(t *testing.T) { 58 | conn, err := postgres.NewTestConnect() 59 | assert.NoError(t, err) 60 | 61 | got, err := readMeminfoRemote(conn) 62 | assert.NoError(t, err) 63 | assert.Greater(t, got.MemTotal, uint64(0)) 64 | assert.Greater(t, got.MemCached, uint64(0)) 65 | assert.Greater(t, got.MemUsed, uint64(0)) 66 | 67 | conn.Close() 68 | _, err = readMeminfoRemote(conn) 69 | assert.Error(t, err) 70 | } 71 | -------------------------------------------------------------------------------- /internal/stat/stat_test.go: -------------------------------------------------------------------------------- 1 | package stat 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/lesovsky/pgcenter/internal/query" 6 | "github.com/lesovsky/pgcenter/internal/view" 7 | "github.com/stretchr/testify/assert" 8 | "regexp" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestNewCollector(t *testing.T) { 14 | conn, err := postgres.NewTestConnect() 15 | assert.NoError(t, err) 16 | 17 | c, err := NewCollector(conn) 18 | assert.NoError(t, err) 19 | assert.NotNil(t, c) 20 | 21 | conn.Close() 22 | c, err = NewCollector(conn) 23 | assert.Error(t, err) 24 | assert.Nil(t, c) 25 | } 26 | 27 | func TestCollector_Update(t *testing.T) { 28 | conn, err := postgres.NewTestConnect() 29 | assert.NoError(t, err) 30 | defer conn.Close() 31 | 32 | props, err := GetPostgresProperties(conn) 33 | assert.NoError(t, err) 34 | 35 | views := view.Views{ 36 | "activity": { 37 | Name: "activity", 38 | QueryTmpl: query.PgStatActivityDefault, 39 | DiffIntvl: [2]int{0, 0}, 40 | Ncols: 14, 41 | OrderKey: 0, 42 | OrderDesc: true, 43 | ColsWidth: map[int]int{}, 44 | Msg: "Show activity statistics", 45 | Filters: map[int]*regexp.Regexp{}, 46 | Refresh: 1 * time.Second, 47 | }, 48 | } 49 | opts := query.NewOptions(props.VersionNum, props.Recovery, props.GucTrackCommitTimestamp, 256, "public") 50 | assert.NoError(t, views.Configure(opts)) 51 | 52 | c, err := NewCollector(conn) 53 | assert.NoError(t, err) 54 | assert.NotNil(t, c) 55 | c.config.collectExtra = CollectDiskstats 56 | 57 | stat, err := c.Update(conn, views["activity"], time.Second) 58 | assert.NoError(t, err) 59 | assert.NotNil(t, stat) 60 | 61 | assert.NotEqual(t, float64(0), stat.System.LoadAvg.One) 62 | assert.NotEqual(t, float64(0), stat.System.Meminfo.MemUsed) 63 | assert.NotEqual(t, float64(0), stat.System.CpuStat.User) 64 | assert.NotEqual(t, 0, len(stat.System.Diskstats)) 65 | assert.NotEqual(t, float64(0), stat.Pgstat.Activity.ConnTotal) 66 | assert.True(t, stat.Pgstat.Result.Valid) 67 | assert.NotEqual(t, 0, len(stat.Pgstat.Result.Values)) 68 | assert.NotEqual(t, 0, len(stat.Pgstat.Result.Cols)) 69 | } 70 | 71 | func TestCollector_collectDiskstats(t *testing.T) { 72 | conn, err := postgres.NewTestConnect() 73 | assert.NoError(t, err) 74 | defer conn.Close() 75 | 76 | c, err := NewCollector(conn) 77 | assert.NoError(t, err) 78 | assert.NotNil(t, c) 79 | 80 | diskstats, err := c.collectDiskstats(conn) 81 | assert.NoError(t, err) 82 | assert.NotNil(t, diskstats) 83 | assert.Greater(t, len(diskstats), 0) 84 | } 85 | 86 | func TestCollector_collectNetdevs(t *testing.T) { 87 | conn, err := postgres.NewTestConnect() 88 | assert.NoError(t, err) 89 | defer conn.Close() 90 | 91 | c, err := NewCollector(conn) 92 | assert.NoError(t, err) 93 | assert.NotNil(t, c) 94 | 95 | netdevs, err := c.collectNetdevs(conn) 96 | assert.NoError(t, err) 97 | assert.NotNil(t, netdevs) 98 | assert.Greater(t, len(netdevs), 0) 99 | } 100 | 101 | func Test_readUptimeLocal(t *testing.T) { 102 | ticks, err := getSysticksLocal() 103 | assert.NoError(t, err) 104 | assert.NotEqual(t, float64(0), ticks) 105 | 106 | got, err := readUptimeLocal("testdata/proc/uptime.golden", ticks) 107 | assert.NoError(t, err) 108 | assert.Equal(t, float64(170191868), got) 109 | 110 | _, err = readUptimeLocal("testdata/proc/stat.golden", ticks) 111 | assert.Error(t, err) 112 | } 113 | 114 | func Test_getSysticksLocal(t *testing.T) { 115 | ticks, err := getSysticksLocal() 116 | assert.NoError(t, err) 117 | assert.NotEqual(t, float64(0), ticks) 118 | } 119 | 120 | func Test_sValue(t *testing.T) { 121 | testcases := []struct { 122 | prev float64 123 | curr float64 124 | itv float64 125 | ticks float64 126 | want float64 127 | }{ 128 | {prev: 1000, curr: 2000, itv: 100, ticks: 100, want: 1000}, // delta 1000 per second within 1 second 129 | {prev: 1000, curr: 5000, itv: 100, ticks: 100, want: 4000}, // delta 4000 per second within 1 second 130 | {prev: 1000, curr: 5000, itv: 400, ticks: 100, want: 1000}, // delta 1000 per second within 4 second 131 | {prev: 2000, curr: 1000, want: 0}, // nothing, current less than previous 132 | } 133 | 134 | for _, tc := range testcases { 135 | assert.Equal(t, tc.want, sValue(tc.prev, tc.curr, tc.itv, tc.ticks)) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /internal/stat/testdata/log/postgresql.log: -------------------------------------------------------------------------------- 1 | 2020-12-05 00:03:46 +05 [1361]: [6517-1] LOG: checkpoint starting: time 2 | 2020-12-05 00:06:16 +05 [1361]: [6518-1] LOG: checkpoint complete: wrote 3034 buffers (18.5%); 0 WAL file(s) added, 0 removed, 1 recycled; write=148.920 s, sync=0.331 s, total=149.951 s; sync files=13, longest=0.172 s, average=0.025 s; distance=14294 kB, estimate=15221 kB 3 | 2020-12-05 00:23:49 +05 [3496012]: [1-1] LOG: automatic analyze of table "pgbench.public.pgbench_accounts" system usage: CPU: user: 0.41 s, system: 0.26 s, elapsed: 8.24 s 4 | 2020-12-05 01:03:46 +05 [1361]: [6541-1] LOG: checkpoint starting: time 5 | 2020-12-05 01:06:16 +05 [1361]: [6542-1] LOG: checkpoint complete: wrote 3371 buffers (20.6%); 0 WAL file(s) added, 0 removed, 1 recycled; write=148.822 s, sync=0.124 s, total=149.601 s; sync files=13, longest=0.078 s, average=0.009 s; distance=14809 kB, estimate=14817 kB 6 | 2020-12-05 09:43:26 +05 [3857610]: [1-1] LOG: automatic analyze of table "pgbench.public.pgbench_accounts" system usage: CPU: user: 0.47 s, system: 0.24 s, elapsed: 8.10 s 7 | 2020-12-05 09:43:50 +05 [1361]: [6749-1] LOG: checkpoint starting: time 8 | 2020-12-05 09:46:20 +05 [1361]: [6750-1] LOG: checkpoint complete: wrote 3958 buffers (24.2%); 0 WAL file(s) added, 0 removed, 1 recycled; write=148.482 s, sync=0.201 s, total=150.272 s; sync files=14, longest=0.157 s, average=0.014 s; distance=15838 kB, estimate=15838 kB 9 | 2020-12-05 09:48:50 +05 [1361]: [6751-1] LOG: checkpoint starting: time 10 | 2020-12-05 09:50:33 +05 [3862897]: [1-1] 127.0.0.1(39352)LOG: could not receive data from client: Connection reset by peer 11 | 2020-12-05 09:50:34 +05 [3862933]: [1-1] 127.0.0.1(39374)LOG: could not receive data from client: Connection reset by peer 12 | 2020-12-05 09:51:20 +05 [1361]: [6752-1] LOG: checkpoint complete: wrote 3199 buffers (19.5%); 0 WAL file(s) added, 0 removed, 1 recycled; write=149.151 s, sync=0.286 s, total=150.170 s; sync files=13, longest=0.100 s, average=0.022 s; distance=12650 kB, estimate=15519 kB 13 | 2020-12-05 09:53:50 +05 [1361]: [6753-1] LOG: checkpoint starting: time 14 | 2020-12-05 09:56:20 +05 [1361]: [6754-1] LOG: checkpoint complete: wrote 3570 buffers (21.8%); 0 WAL file(s) added, 0 removed, 1 recycled; write=148.898 s, sync=0.327 s, total=149.897 s; sync files=12, longest=0.155 s, average=0.027 s; distance=15367 kB, estimate=15504 kB 15 | 2020-12-05 09:56:23 +05 [3867158]: [1-1] 127.0.0.1(39578)LOG: could not receive data from client: Connection reset by peer 16 | 2020-12-05 09:56:24 +05 [3867171]: [1-1] 127.0.0.1(39588)LOG: could not receive data from client: Connection reset by peer 17 | 2020-12-05 09:58:50 +05 [1361]: [6755-1] LOG: checkpoint starting: time 18 | 2020-12-05 10:01:20 +05 [1361]: [6756-1] LOG: checkpoint complete: wrote 3483 buffers (21.3%); 0 WAL file(s) added, 0 removed, 1 recycled; write=149.046 s, sync=0.109 s, total=149.671 s; sync files=11, longest=0.080 s, average=0.009 s; distance=14103 kB, estimate=15364 kB 19 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/diskstats.invalid: -------------------------------------------------------------------------------- 1 | 7 0 loop0 95 0 2452 32 0 0 0 0 0 48 2 | 7 1 loop1 4240 0 9320 1752 0 0 0 0 0 548 3 | 8 0 sda 364890 90257 15905820 98256 5112729 4404633 312756632 3722346 0 3986076 4 | 8 16 sdb 307367 16786 12135182 8494946 84857424 8238986 1047928880 1649683162 0 114626504 5 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/diskstats.v1.golden: -------------------------------------------------------------------------------- 1 | 7 0 loop0 95 0 2452 32 0 0 0 0 0 48 4 2 | 7 1 loop1 4240 0 9320 1752 0 0 0 0 0 548 552 3 | 8 0 sda 364890 90257 15905820 98256 5112729 4404633 312756632 3722346 0 3986076 1949872 4 | 8 16 sdb 307367 16786 12135182 8494946 84857424 8238986 1047928880 1649683162 0 114626504 1553521352 5 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/diskstats.v2.2.golden: -------------------------------------------------------------------------------- 1 | 7 0 loop0 95 0 2452 32 0 0 0 0 0 48 4 0 0 0 0 2 | 7 1 loop1 4240 0 9320 1752 0 0 0 0 0 548 552 0 0 0 0 3 | 8 0 sda 365227 90272 15921204 98369 5150395 4436840 318109296 3768215 0 4016024 1972744 391141 0 242608824 1702883 4 | 8 16 sdb 309617 16786 12153678 8579283 85792505 8398482 1060037416 1671784653 0 115793892 1574521168 0 0 0 0 5 | 7 8 loop8 624 0 3532 20 0 0 0 0 0 212 0 0 0 0 0 6 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/diskstats.v2.golden: -------------------------------------------------------------------------------- 1 | 7 0 loop0 95 0 2452 32 0 0 0 0 0 48 4 0 0 0 0 2 | 7 1 loop1 4240 0 9320 1752 0 0 0 0 0 548 552 0 0 0 0 3 | 8 0 sda 365227 90272 15921204 98369 5150316 4436838 318033448 3768000 0 4015784 1972664 391141 0 242608824 1702883 4 | 8 16 sdb 309617 16786 12153678 8579283 85792400 8398482 1060036272 1671783781 0 115793700 1574520404 0 0 0 0 5 | 7 8 loop8 624 0 3532 20 0 0 0 0 0 212 0 0 0 0 0 6 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/diskstats.v3.golden: -------------------------------------------------------------------------------- 1 | 7 0 loop0 95 0 2452 32 0 0 0 0 0 48 4 0 0 0 0 0 0 2 | 7 1 loop1 4240 0 9320 1752 0 0 0 0 0 548 552 0 0 0 0 0 0 3 | 8 0 sda 365208 90272 15920380 98361 5144476 4431757 316780704 3758592 0 4010804 1967660 390788 0 242426032 1702032 1452785 1458752145 4 | 8 16 sdb 309388 16786 12151846 8569789 85675301 8382129 1058502704 1669180952 0 115656192 1572052888 0 0 0 0 4587 12547854 5 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/loadavg.golden: -------------------------------------------------------------------------------- 1 | 2.43 2.30 1.74 1/1697 2277691 2 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/loadavg.invalid: -------------------------------------------------------------------------------- 1 | 2.43 2.30 2 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/meminfo.golden: -------------------------------------------------------------------------------- 1 | MemTotal: 32839484 kB 2 | MemFree: 21570088 kB 3 | MemAvailable: 26190600 kB 4 | Buffers: 604064 kB 5 | Cached: 4361844 kB 6 | SwapCached: 0 kB 7 | Active: 7785324 kB 8 | Inactive: 2591484 kB 9 | Active(anon): 5448748 kB 10 | Inactive(anon): 344784 kB 11 | Active(file): 2336576 kB 12 | Inactive(file): 2246700 kB 13 | Unevictable: 0 kB 14 | Mlocked: 0 kB 15 | SwapTotal: 16777212 kB 16 | SwapFree: 16777212 kB 17 | Dirty: 36404 kB 18 | Writeback: 0 kB 19 | AnonPages: 5410948 kB 20 | Mapped: 1197820 kB 21 | Shmem: 386884 kB 22 | KReclaimable: 502080 kB 23 | Slab: 692516 kB 24 | SReclaimable: 502080 kB 25 | SUnreclaim: 190436 kB 26 | KernelStack: 16848 kB 27 | PageTables: 54472 kB 28 | NFS_Unstable: 0 kB 29 | Bounce: 0 kB 30 | WritebackTmp: 0 kB 31 | CommitLimit: 33196952 kB 32 | Committed_AS: 12808144 kB 33 | VmallocTotal: 34359738367 kB 34 | VmallocUsed: 34976 kB 35 | VmallocChunk: 0 kB 36 | Percpu: 6528 kB 37 | HardwareCorrupted: 0 kB 38 | AnonHugePages: 0 kB 39 | ShmemHugePages: 0 kB 40 | ShmemPmdMapped: 0 kB 41 | FileHugePages: 0 kB 42 | FilePmdMapped: 0 kB 43 | CmaTotal: 0 kB 44 | CmaFree: 0 kB 45 | HugePages_Total: 0 46 | HugePages_Free: 0 47 | HugePages_Rsvd: 0 48 | HugePages_Surp: 0 49 | Hugepagesize: 2048 kB 50 | Hugetlb: 0 kB 51 | DirectMap4k: 482128 kB 52 | DirectMap2M: 13101056 kB 53 | DirectMap1G: 19922944 kB 54 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/netdev.invalid.1: -------------------------------------------------------------------------------- 1 | Inter-| Receive | Transmit 2 | face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed 3 | br-63732f2fc81f: 197975757 583782 10 20 30 40 50 60 8688001214 1460628 70 80 90 100 110 4 | vetha6f6db1: 77175306 225055 0 0 0 0 0 0 74337926 422379 0 0 0 0 0 5 | veth681540b: 0 0 0 0 0 0 0 0 7861673 41751 0 0 0 0 0 6 | vethb1d564c: 10221704004 3861145 0 0 0 0 0 0 5018467810 3350929 0 0 0 0 0 7 | wlx98482780ac74: 19442146228 14953729 15 90218 25 35 45 55 653429694 3893477 65 75 85 95 105 8 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/netdev.invalid.2: -------------------------------------------------------------------------------- 1 | Inter-| Receive | Transmit 2 | face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed 3 | br-1234567: 197975757 583782 invalid 20 30 40 50 60 8688001214 1460628 70 80 90 100 110 120 4 | vetha6f6db1: 77175306 225055 0 0 0 0 0 0 74337926 422379 0 0 0 0 0 0 5 | veth681540b: 0 0 0 0 0 0 0 0 7861673 41751 0 0 0 0 0 0 6 | vethb1d564c: 10221704004 3861145 0 0 0 0 0 0 5018467810 3350929 0 0 0 0 0 0 7 | wlx1234567: 19442146228 14953729 15 invalid 35 45 55 65 653429694 3893477 75 85 95 105 115 125 8 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/netdev.v1.golden: -------------------------------------------------------------------------------- 1 | Inter-| Receive | Transmit 2 | face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed 3 | br-1234567: 197975757 583782 10 20 30 40 50 60 8688001214 1460628 70 80 90 100 110 120 4 | vetha6f6db1: 77175306 225055 0 0 0 0 0 0 74337926 422379 0 0 0 0 0 0 5 | veth681540b: 0 0 0 0 0 0 0 0 7861673 41751 0 0 0 0 0 0 6 | vethb1d564c: 10221704004 3861145 0 0 0 0 0 0 5018467810 3350929 0 0 0 0 0 0 7 | wlx1234567: 19442146228 14953729 15 25 35 45 55 65 653429694 3893477 75 85 95 105 115 125 8 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/netdev.v2.golden: -------------------------------------------------------------------------------- 1 | Inter-| Receive | Transmit 2 | face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed 3 | br-1234567: 197988130 583968 458 125 245 42 4582 248 8693944658 1461271 421 458 256 785 485 547 4 | vetha6f6db1: 77210195 225136 0 0 0 0 0 0 74368060 422541 0 0 0 0 0 0 5 | veth681540b: 0 0 0 0 0 0 0 0 7866431 41776 0 0 0 0 0 0 6 | vethb1d564c: 10229088765 3863777 0 0 0 0 0 0 5022145302 3353289 0 0 0 0 0 0 7 | wlx1234567: 19560963123 15078842 54 48 478 125 355 455 834982407 4024235 487 755 985 475 500 875 8 | -------------------------------------------------------------------------------- /internal/stat/testdata/proc/uptime.golden: -------------------------------------------------------------------------------- 1 | 1701918.68 10948222.30 2 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | const ( 4 | // programName is the name of this program. 5 | programName = "pgcenter" 6 | ) 7 | 8 | var ( 9 | // Git variables imported at build stage. 10 | gitTag, gitCommit, gitBranch string 11 | ) 12 | 13 | // Version returns the name and version information of this program. 14 | func Version() (string, string, string, string) { 15 | return programName, gitTag, gitCommit, gitBranch 16 | } 17 | -------------------------------------------------------------------------------- /profile/testdata/profile_header.golden: -------------------------------------------------------------------------------- 1 | ------ ------------ ----------------------------- 2 | % time seconds wait_event query: SELECT f1, f2, f3 FROM t1, t2 WHERE t1.f1 = t2.f1 3 | ------ ------------ ----------------------------- 4 | -------------------------------------------------------------------------------- /profile/testdata/profile_stats.golden: -------------------------------------------------------------------------------- 1 | 85.10 8510.000000 Running 2 | 10.20 1020.000000 Test.Entry1 3 | 3.30 330.000000 Test.Entry2 4 | 1.40 140.000000 Test.Entry3 5 | ------ ------------ ----------------------------- 6 | 100.00 30000.000000 including workers 7 | 10000.000000 8 | -------------------------------------------------------------------------------- /record/record_test.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "archive/tar" 5 | "github.com/lesovsky/pgcenter/internal/postgres" 6 | "github.com/lesovsky/pgcenter/internal/view" 7 | "github.com/stretchr/testify/assert" 8 | "io" 9 | "os" 10 | "os/signal" 11 | "path/filepath" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func Test_app_setup(t *testing.T) { 17 | dbconfig, err := postgres.NewTestConfig() 18 | assert.NoError(t, err) 19 | 20 | app := newApp(Config{OutputFile: "/tmp/pgcenter-record-testing.stat.tar"}, dbconfig) 21 | 22 | assert.NoError(t, app.setup()) 23 | 24 | assert.NotNil(t, app.views) // views must not be nil 25 | assert.Greater(t, len(app.views), 0) // views must contains view objects 26 | for _, v := range app.views { 27 | assert.NotEqual(t, "", v.Query) // view's queries must not be empty (must be created using templates) 28 | } 29 | assert.NotNil(t, app.recorder) 30 | } 31 | 32 | func Test_app_record(t *testing.T) { 33 | filename := "/tmp/pgcenter-record-testing.stat.tar" 34 | totalViews := len(view.New()) + 1 // stats + metadata 35 | count, itv := 2, time.Second // recording settings 36 | 37 | testcases := []struct { 38 | name string 39 | config Config 40 | filesWant int 41 | }{ 42 | { 43 | // a new archive should be created with 44 | name: "append to new file", 45 | config: Config{Count: count, Interval: itv, OutputFile: filename, AppendFile: false}, 46 | filesWant: totalViews * count, 47 | }, 48 | { 49 | // append to existing file, previously written files should be kept. 50 | name: "append to existing file", 51 | config: Config{Count: count, Interval: itv, OutputFile: filename, AppendFile: true}, 52 | filesWant: (totalViews * count) * 2, // doubles because files are from previous test. 53 | }, 54 | { 55 | // truncate existing file and write new stats 56 | name: "truncate existing file", 57 | config: Config{Count: count, Interval: itv, OutputFile: filename, AppendFile: false}, 58 | filesWant: totalViews * count, 59 | }, 60 | } 61 | 62 | // initial stuff 63 | doQuit := make(chan os.Signal, 1) 64 | signal.Notify(doQuit, os.Interrupt) 65 | 66 | dbconfig, err := postgres.NewTestConfig() 67 | assert.NoError(t, err) 68 | 69 | for _, tc := range testcases { 70 | t.Run(tc.name, func(t *testing.T) { 71 | app := newApp(tc.config, dbconfig) 72 | assert.NoError(t, app.setup()) 73 | 74 | assert.NoError(t, app.record(doQuit)) 75 | 76 | // Read written stats. 77 | f, err := os.Open(filepath.Clean(filename)) 78 | assert.NoError(t, err) 79 | tr := tar.NewReader(f) 80 | 81 | var filesCount int 82 | for { 83 | hdr, err := tr.Next() 84 | if err == io.EOF { 85 | break 86 | } 87 | filesCount++ 88 | assert.NoError(t, err) 89 | assert.Greater(t, hdr.Size, int64(0)) 90 | } 91 | assert.Greater(t, filesCount, 0) 92 | assert.Equal(t, tc.filesWant, filesCount) 93 | }) 94 | } 95 | assert.NoError(t, os.Remove(filename)) 96 | } 97 | 98 | func Test_filterViews(t *testing.T) { 99 | testcases := []struct { 100 | version int 101 | pgssSchema string 102 | wantN int 103 | wantV int 104 | }{ 105 | {version: 140000, pgssSchema: "", wantN: 6, wantV: 15}, 106 | {version: 140000, pgssSchema: "public", wantN: 0, wantV: 21}, 107 | {version: 130000, pgssSchema: "public", wantN: 3, wantV: 18}, 108 | {version: 120000, pgssSchema: "public", wantN: 6, wantV: 15}, 109 | {version: 110000, pgssSchema: "public", wantN: 8, wantV: 13}, 110 | {version: 100000, pgssSchema: "public", wantN: 8, wantV: 13}, 111 | } 112 | 113 | for _, tc := range testcases { 114 | n, v := filterViews(tc.version, tc.pgssSchema, view.New()) 115 | assert.Equal(t, tc.wantN, n) 116 | assert.Equal(t, tc.wantV, len(v)) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /record/recorder_test.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "archive/tar" 5 | "database/sql" 6 | "encoding/json" 7 | "github.com/lesovsky/pgcenter/internal/postgres" 8 | "github.com/lesovsky/pgcenter/internal/query" 9 | "github.com/lesovsky/pgcenter/internal/stat" 10 | "github.com/lesovsky/pgcenter/internal/view" 11 | "github.com/stretchr/testify/assert" 12 | "io" 13 | "os" 14 | "path/filepath" 15 | "testing" 16 | "time" 17 | ) 18 | 19 | func Test_tarRecorder_open_close(t *testing.T) { 20 | tc := newTarRecorder(tarConfig{filename: "/tmp/pgcenter-record-testing.stat.tar", append: false}) 21 | assert.NoError(t, tc.open()) 22 | assert.NoError(t, tc.close()) 23 | 24 | tc = newTarRecorder(tarConfig{filename: "/tmp/pgcenter-record-testing.stat.tar", append: true}) 25 | assert.NoError(t, tc.open()) 26 | assert.NoError(t, tc.close()) 27 | } 28 | 29 | func Test_tarRecorder(t *testing.T) { 30 | tc := newTarRecorder(tarConfig{filename: "/tmp/pgcenter-record-testing.stat.tar"}) 31 | assert.NoError(t, tc.open()) 32 | 33 | // create and configure views 34 | db, err := postgres.NewTestConnect() 35 | assert.NoError(t, err) 36 | props, err := stat.GetPostgresProperties(db) 37 | assert.NoError(t, err) 38 | views := view.New() 39 | opts := query.NewOptions(props.VersionNum, props.Recovery, props.GucTrackCommitTimestamp, 0, "public") 40 | assert.NoError(t, views.Configure(opts)) 41 | db.Close() 42 | 43 | // create postgres config 44 | dbConfig, err := postgres.NewTestConfig() 45 | assert.NoError(t, err) 46 | stats, err := tc.collect(dbConfig, views) 47 | assert.NoError(t, err) 48 | assert.NotNil(t, stats) 49 | 50 | // check all stats have filled columns 51 | for _, s := range stats { 52 | assert.Greater(t, len(s.Cols), 0) 53 | } 54 | 55 | assert.NoError(t, tc.close()) 56 | } 57 | 58 | func Test_tarRecorder_write(t *testing.T) { 59 | stats := map[string]stat.PGresult{ 60 | "pgcenter_record_testing": { 61 | Valid: true, Ncols: 2, Nrows: 4, Cols: []string{"col1", "col2"}, 62 | Values: [][]sql.NullString{ 63 | {{String: "alfa", Valid: true}, {String: "12.06157", Valid: true}}, 64 | {{String: "bravo", Valid: true}, {String: "819.188", Valid: true}}, 65 | {{String: "charli", Valid: true}, {String: "18.126", Valid: true}}, 66 | {{String: "delta", Valid: true}, {String: "137.176", Valid: true}}, 67 | }, 68 | }, 69 | } 70 | 71 | filename := "/tmp/pgcenter-record-testing.stat.tar" 72 | 73 | // Write testdata. 74 | tc := newTarRecorder(tarConfig{filename: filename, append: false}) 75 | assert.NoError(t, tc.open()) 76 | assert.NoError(t, tc.write(stats)) 77 | assert.NoError(t, tc.close()) 78 | 79 | // Read written testdata and compare with origin testdata. 80 | f, err := os.Open(filepath.Clean(filename)) // open file 81 | assert.NoError(t, err) 82 | assert.NotNil(t, f) 83 | 84 | tr := tar.NewReader(f) // create tar reader 85 | hdr, err := tr.Next() 86 | assert.NoError(t, err) 87 | data := make([]byte, hdr.Size) // make data buffer 88 | _, err = io.ReadFull(tr, data) // read data from tar to buffer 89 | assert.NoError(t, err) 90 | got := stat.PGresult{} 91 | assert.NoError(t, json.Unmarshal(data, &got)) // unmarshal to JSON 92 | assert.Equal(t, stats, map[string]stat.PGresult{"pgcenter_record_testing": got}) // compare unmarshalled with origin 93 | 94 | // Cleanup. 95 | assert.NoError(t, os.Remove(filename)) 96 | } 97 | 98 | func Test_newFilenameString(t *testing.T) { 99 | testcases := []struct { 100 | ts time.Time 101 | want string 102 | }{ 103 | {ts: time.Date(2021, 06, 15, 12, 30, 15, 123456789, time.UTC), want: "example.20210615T123015.123.json"}, 104 | {ts: time.Date(2021, 06, 15, 12, 30, 15, 23456789, time.UTC), want: "example.20210615T123015.023.json"}, 105 | {ts: time.Date(2021, 06, 15, 12, 30, 15, 3456789, time.UTC), want: "example.20210615T123015.003.json"}, 106 | {ts: time.Date(2021, 06, 15, 12, 30, 15, 456789, time.UTC), want: "example.20210615T123015.000.json"}, 107 | {ts: time.Date(2021, 06, 15, 12, 30, 15, 789, time.UTC), want: "example.20210615T123015.000.json"}, 108 | {ts: time.Date(2021, 06, 15, 12, 30, 15, 0, time.UTC), want: "example.20210615T123015.000.json"}, 109 | } 110 | 111 | for _, tc := range testcases { 112 | assert.Equal(t, tc.want, newFilenameString(tc.ts, "example")) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /report/testdata/README.md: -------------------------------------------------------------------------------- 1 | ### Testing notes 2 | 3 | #### How to create golden report with valid order of archived files. 4 | ``` 5 | tar cf pgcenter.stat.golden.tar $(for ts in $(ls |cut -d. -f2 |sort -u); do ls meta.$ts.123.json; ls *.$ts.123.json |grep -v meta; done |xargs) 6 | ``` -------------------------------------------------------------------------------- /report/testdata/report_activity_grep.golden: -------------------------------------------------------------------------------- 1 | pid cl_addr cl_port datname usename appname backend_type wait_etype wait_event state xact_age query_age change_age query  2 | 2021/06/14 11:56:42, rate: 1s 3 | 3781555 -1 pgbench lesovsky client backend active 00:00:00 00:00:00 00:00:00 SELECT pid, client_addr AS cl_a~ 4 | 3781498 -1 pgbench postgres pgbench client backend active 00:00:00 00:00:00 00:00:00 SELECT abalance FROM pgbench_ac~ 5 | -------------------------------------------------------------------------------- /report/testdata/report_activity_order_pid_asc.golden: -------------------------------------------------------------------------------- 1 | pid cl_addr cl_port datname usename appname backend_type wait_etype wait_event state xact_age query_age change_age query  2 | 2021/06/14 11:56:42, rate: 1s 3 | 2967718 127.0.0.1/32 35960 postgres 14/replica walsender Activity WalSenderMain active 3 days 18:14:38 3 days 18:14:38 START_REPLICATION 36/E3000000 T~ 4 | 3781488 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 5 | 3781489 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 6 | 3781490 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 7 | 3781491 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 8 | 3781492 -1 pgbench postgres pgbench client backend IO WALSync active 00:00:00 00:00:00 00:00:00 END; 9 | 3781493 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 10 | 3781494 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 11 | 3781495 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 12 | 3781498 -1 pgbench postgres pgbench client backend active 00:00:00 00:00:00 00:00:00 SELECT abalance FROM pgbench_ac~ 13 | 3781555 -1 pgbench lesovsky client backend active 00:00:00 00:00:00 00:00:00 SELECT pid, client_addr AS cl_a~ 14 | -------------------------------------------------------------------------------- /report/testdata/report_activity_order_pid_desc.golden: -------------------------------------------------------------------------------- 1 | pid cl_addr cl_port datname usename appname backend_type wait_etype wait_event state xact_age query_age change_age query  2 | 2021/06/14 11:56:42, rate: 1s 3 | 3781555 -1 pgbench lesovsky client backend active 00:00:00 00:00:00 00:00:00 SELECT pid, client_addr AS cl_a~ 4 | 3781498 -1 pgbench postgres pgbench client backend active 00:00:00 00:00:00 00:00:00 SELECT abalance FROM pgbench_ac~ 5 | 3781495 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 6 | 3781494 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 7 | 3781493 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 8 | 3781492 -1 pgbench postgres pgbench client backend IO WALSync active 00:00:00 00:00:00 00:00:00 END; 9 | 3781491 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 10 | 3781490 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 11 | 3781489 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 12 | 3781488 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 13 | 2967718 127.0.0.1/32 35960 postgres 14/replica walsender Activity WalSenderMain active 3 days 18:14:38 3 days 18:14:38 START_REPLICATION 36/E3000000 T~ 14 | -------------------------------------------------------------------------------- /report/testdata/report_activity_start_end.golden: -------------------------------------------------------------------------------- 1 | pid cl_addr cl_port datname usename appname backend_type wait_etype wait_event state xact_age query_age change_age query  2 | 2021/06/14 11:56:42, rate: 1s 3 | 3781555 -1 pgbench lesovsky client backend active 00:00:00 00:00:00 00:00:00 SELECT pid, client_addr AS cl_a~ 4 | 3781498 -1 pgbench postgres pgbench client backend active 00:00:00 00:00:00 00:00:00 SELECT abalance FROM pgbench_ac~ 5 | 3781495 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 6 | 3781494 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 7 | 3781493 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 8 | 3781492 -1 pgbench postgres pgbench client backend IO WALSync active 00:00:00 00:00:00 00:00:00 END; 9 | 3781491 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 10 | 3781490 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 11 | 3781489 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 12 | 3781488 -1 pgbench postgres pgbench client backend LWLock WALWrite active 00:00:00 00:00:00 00:00:00 END; 13 | 2967718 127.0.0.1/32 35960 postgres 14/replica walsender Activity WalSenderMain active 3 days 18:14:38 3 days 18:14:38 START_REPLICATION 36/E3000000 T~ 14 | -------------------------------------------------------------------------------- /report/testdata/report_entry_sample.golden: -------------------------------------------------------------------------------- 1 | 0001/01/01 00:00:00, rate: 1s 2 | example_db 5423 24 8452 8452145 45214 58452 4521 45221 45854 248 785 2 4774 698785411 4582.02 42.12 10 days 10:10:10 3 | example_db2 84521 866 59654 485421 86421 89642 9869 45212 96969 124 858 0 8457 6581546 2445.77 458.01 10 days 10:10:10 4 | -------------------------------------------------------------------------------- /report/testdata/report_functions.golden: -------------------------------------------------------------------------------- 1 | funcid function calls_total calls total self total_avg,ms self_avg,ms  2 | 2021/06/14 11:56:34, rate: 1s 3 | 26082 public.pg_stat_statements 72054 6 00:02:53 00:02:53 2.4100 2.4100 4 | 1215 pg_catalog.obj_description 12 0 00:00:00 00:00:00 1.3700 1.3700 5 | 2021/06/14 11:56:35, rate: 1s 6 | 26082 public.pg_stat_statements 72060 6 00:02:53 00:02:53 2.4100 2.4100 7 | 1215 pg_catalog.obj_description 12 0 00:00:00 00:00:00 1.3700 1.3700 8 | 2021/06/14 11:56:36, rate: 1s 9 | 26082 public.pg_stat_statements 72066 6 00:02:53 00:02:53 2.4100 2.4100 10 | 1215 pg_catalog.obj_description 12 0 00:00:00 00:00:00 1.3700 1.3700 11 | 2021/06/14 11:56:37, rate: 1s 12 | 26082 public.pg_stat_statements 72072 6 00:02:53 00:02:53 2.4100 2.4100 13 | 1215 pg_catalog.obj_description 12 0 00:00:00 00:00:00 1.3700 1.3700 14 | 2021/06/14 11:56:38, rate: 1s 15 | 26082 public.pg_stat_statements 72078 6 00:02:53 00:02:53 2.4100 2.4100 16 | 1215 pg_catalog.obj_description 12 0 00:00:00 00:00:00 1.3700 1.3700 17 | 2021/06/14 11:56:39, rate: 1s 18 | 26082 public.pg_stat_statements 72084 6 00:02:53 00:02:53 2.4100 2.4100 19 | 1215 pg_catalog.obj_description 12 0 00:00:00 00:00:00 1.3700 1.3700 20 | 2021/06/14 11:56:40, rate: 1s 21 | 26082 public.pg_stat_statements 72090 6 00:02:53 00:02:53 2.4100 2.4100 22 | 1215 pg_catalog.obj_description 12 0 00:00:00 00:00:00 1.3700 1.3700 23 | 2021/06/14 11:56:41, rate: 1s 24 | 26082 public.pg_stat_statements 72096 6 00:02:53 00:02:53 2.4100 2.4100 25 | 1215 pg_catalog.obj_description 12 0 00:00:00 00:00:00 1.3700 1.3700 26 | 2021/06/14 11:56:42, rate: 1s 27 | 26082 public.pg_stat_statements 72102 6 00:02:53 00:02:53 2.4100 2.4100 28 | 1215 pg_catalog.obj_description 12 0 00:00:00 00:00:00 1.3700 1.3700 29 | -------------------------------------------------------------------------------- /report/testdata/report_indexes.golden: -------------------------------------------------------------------------------- 1 | index scan tuples_read tuples_fetch hit read,KiB  2 | 2021/06/14 11:56:34, rate: 1s 3 | public.pgbench_tellers.pgbench_~ 35 35 35 69 8 4 | public.pgbench_branches.pgbench~ 35 35 35 35 0 5 | public.pgbench_accounts.pgbench~ 23277 23369 23277 51061 151392 6 | 2021/06/14 11:56:35, rate: 1s 7 | public.pgbench_tellers.pgbench_~ 53 53 53 106 0 8 | public.pgbench_branches.pgbench~ 53 53 53 53 0 9 | public.pgbench_accounts.pgbench~ 32721 32836 32721 71776 212768 10 | 2021/06/14 11:56:36, rate: 1s 11 | public.pgbench_tellers.pgbench_~ 37 37 37 73 8 12 | public.pgbench_branches.pgbench~ 37 37 37 37 0 13 | public.pgbench_accounts.pgbench~ 25971 26055 25971 56896 169336 14 | 2021/06/14 11:56:37, rate: 1s 15 | public.pgbench_tellers.pgbench_~ 34 34 34 67 8 16 | public.pgbench_branches.pgbench~ 34 34 34 34 0 17 | public.pgbench_accounts.pgbench~ 26479 26565 26479 58089 171952 18 | 2021/06/14 11:56:38, rate: 1s 19 | public.pgbench_tellers.pgbench_~ 39 39 39 77 8 20 | public.pgbench_branches.pgbench~ 39 39 39 39 0 21 | public.pgbench_accounts.pgbench~ 22666 22740 22666 49776 146896 22 | 2021/06/14 11:56:39, rate: 1s 23 | public.pgbench_tellers.pgbench_~ 33 33 33 66 0 24 | public.pgbench_branches.pgbench~ 33 33 33 33 0 25 | public.pgbench_accounts.pgbench~ 22660 22748 22660 49729 147208 26 | 2021/06/14 11:56:40, rate: 1s 27 | public.pgbench_tellers.pgbench_~ 37 37 37 75 8 28 | public.pgbench_branches.pgbench~ 37 37 37 37 0 29 | public.pgbench_accounts.pgbench~ 11897 11960 11897 26085 77760 30 | index scan tuples_read tuples_fetch hit read,KiB  31 | 2021/06/14 11:56:41, rate: 1s 32 | public.pgbench_tellers.pgbench_~ 32 32 32 63 8 33 | public.pgbench_branches.pgbench~ 32 32 32 32 0 34 | public.pgbench_accounts.pgbench~ 29633 29730 29633 65063 191880 35 | 2021/06/14 11:56:42, rate: 1s 36 | public.pgbench_tellers.pgbench_~ 52 52 52 103 8 37 | public.pgbench_branches.pgbench~ 52 52 52 52 0 38 | public.pgbench_accounts.pgbench~ 26425 26519 26425 57938 172136 39 | -------------------------------------------------------------------------------- /report/testdata/report_progress_analyze.golden: -------------------------------------------------------------------------------- 1 | pid xact_age datname relation state waiting phase sample_size,KiB scanned,% ext_total/done child_total/done,% child_in_progress  2 | 2021/06/14 11:56:34, rate: 1s 3 | 3788780 00:00:02 pgbench pgbench_accounts active IO.DataFileRead acquiring sample rows 1967216 11.00 0/0 0/0.00 - 4 | 2021/06/14 11:56:35, rate: 1s 5 | 3788780 00:00:03 pgbench pgbench_accounts active IO.DataFileRead acquiring sample rows 1967216 15.00 0/0 0/0.00 - 6 | 2021/06/14 11:56:36, rate: 1s 7 | 3788780 00:00:04 pgbench pgbench_accounts active IO.DataFileRead acquiring sample rows 1967216 20.00 0/0 0/0.00 - 8 | 2021/06/14 11:56:37, rate: 1s 9 | 3788780 00:00:05 pgbench pgbench_accounts active f acquiring sample rows 1967216 27.00 0/0 0/0.00 - 10 | 2021/06/14 11:56:38, rate: 1s 11 | 3788780 00:00:06 pgbench pgbench_accounts active IO.DataFileRead acquiring sample rows 1967216 33.00 0/0 0/0.00 - 12 | 2021/06/14 11:56:39, rate: 1s 13 | 3788780 00:00:07 pgbench pgbench_accounts active f acquiring sample rows 1967216 39.00 0/0 0/0.00 - 14 | 2021/06/14 11:56:40, rate: 1s 15 | 3788780 00:00:08 pgbench pgbench_accounts active f acquiring sample rows 1967216 45.00 0/0 0/0.00 - 16 | 2021/06/14 11:56:41, rate: 1s 17 | 3788780 00:00:09 pgbench pgbench_accounts active IO.DataFilePre~ acquiring sample rows 1967216 52.00 0/0 0/0.00 - 18 | 2021/06/14 11:56:42, rate: 1s 19 | 3788780 00:00:10 pgbench pgbench_accounts active IO.DataFileRead acquiring sample rows 1967216 59.00 0/0 0/0.00 - 20 | -------------------------------------------------------------------------------- /report/testdata/report_progress_basebackup.golden: -------------------------------------------------------------------------------- 1 | pid started_from started_at duration state waiting phase size_total,KiB streamed,% streamed,KiB tablespaces_total/streamed  2 | 2021/06/14 11:56:34, rate: 1s 3 | 3793153 127.0.0.1/32 2021-06-14 12:38:56 00:00:02 active f streaming database files 2455099 6.00 134641 1/0 4 | 2021/06/14 11:56:35, rate: 1s 5 | 3793153 127.0.0.1/32 2021-06-14 12:38:56 00:00:03 active IO.Base~ streaming database files 2455099 12.00 144000 1/0 6 | 2021/06/14 11:56:36, rate: 1s 7 | 3793153 127.0.0.1/32 2021-06-14 12:38:56 00:00:04 active Client.~ streaming database files 2455099 33.00 535473 1/0 8 | 2021/06/14 11:56:37, rate: 1s 9 | 3793153 127.0.0.1/32 2021-06-14 12:38:56 00:00:05 active IO.Base~ streaming database files 2455099 58.00 611195 1/0 10 | 2021/06/14 11:56:38, rate: 1s 11 | 3793153 127.0.0.1/32 2021-06-14 12:38:56 00:00:06 active f streaming database files 2455099 76.00 439659 1/0 12 | 2021/06/14 11:56:39, rate: 1s 13 | 3793153 127.0.0.1/32 2021-06-14 12:38:56 00:00:07 active IO.Base~ streaming database files 2455099 98.00 527648 1/0 14 | 2021/06/14 11:56:40, rate: 1s 15 | 3793153 127.0.0.1/32 2021-06-14 12:38:56 00:00:08 active IO.Base~ streaming database files 2455099 98.00 20190 1/0 16 | 2021/06/14 11:56:41, rate: 1s 17 | 3793153 127.0.0.1/32 2021-06-14 12:38:56 00:00:09 active IO.Base~ streaming database files 2455099 99.00 1964 1/0 18 | 2021/06/14 11:56:42, rate: 1s 19 | 3793153 127.0.0.1/32 2021-06-14 12:38:56 00:00:10 active IO.Base~ streaming database files 2455099 99.00 1309 1/0 20 | -------------------------------------------------------------------------------- /report/testdata/report_progress_cluster.golden: -------------------------------------------------------------------------------- 1 | pid xact_age datname relation index state waiting phase size_total,KiB scanned_total,% tuples_scanned tuples_written query  2 | 2021/06/14 11:56:34, rate: 1s 3 | 3788780 00:00:06 pgbench pgbench_accounts - active IO.WALSync seq scanning heap 3781592 59.00 502152 502152 vacuum full pgbench_accounts ; 4 | 2021/06/14 11:56:35, rate: 1s 5 | 3788780 00:00:07 pgbench pgbench_accounts - active f seq scanning heap 3781592 60.00 383519 383519 vacuum full pgbench_accounts ; 6 | 2021/06/14 11:56:36, rate: 1s 7 | 3788780 00:00:08 pgbench pgbench_accounts - active IO.WALSync seq scanning heap 3781592 62.00 495308 495308 vacuum full pgbench_accounts ; 8 | 2021/06/14 11:56:37, rate: 1s 9 | 3788780 00:00:09 pgbench pgbench_accounts - active IO.WALSync seq scanning heap 3781592 63.00 502152 502152 vacuum full pgbench_accounts ; 10 | 2021/06/14 11:56:38, rate: 1s 11 | 3788780 00:00:10 pgbench pgbench_accounts - active LWLock.WA~ seq scanning heap 3781592 65.00 502213 502213 vacuum full pgbench_accounts ; 12 | 2021/06/14 11:56:39, rate: 1s 13 | 3788780 00:00:11 pgbench pgbench_accounts - active IO.WALSync seq scanning heap 3781592 67.00 502152 502152 vacuum full pgbench_accounts ; 14 | 2021/06/14 11:56:40, rate: 1s 15 | 3788780 00:00:12 pgbench pgbench_accounts - active IO.WALSync seq scanning heap 3781592 69.00 502152 502152 vacuum full pgbench_accounts ; 16 | 2021/06/14 11:56:41, rate: 1s 17 | 3788780 00:00:13 pgbench pgbench_accounts - active LWLock.WA~ seq scanning heap 3781592 70.00 502213 502213 vacuum full pgbench_accounts ; 18 | 2021/06/14 11:56:42, rate: 1s 19 | 3788780 00:00:14 pgbench pgbench_accounts - active f seq scanning heap 3781592 72.00 580719 580719 vacuum full pgbench_accounts ; 20 | -------------------------------------------------------------------------------- /report/testdata/report_progress_copy.golden: -------------------------------------------------------------------------------- 1 | pid xact_age datname relation state waiting command type size_total,KiB source_total,KiB processed,KiB processed,% tuples_processed tuples_excluded  2 | 2021/06/14 11:56:34, rate: 1s 3 | 3793641 00:00:02 tmp 47542 active f COPY FROM PIPE 0 148620 1537084 0 4 | 3793637 00:00:03 pgbench pgbench_accounts active f COPY TO PIPE 1967216 0 148919 1540266 0 5 | 2021/06/14 11:56:35, rate: 1s 6 | 3793641 00:00:03 tmp 47542 active f COPY FROM PIPE 0 196468 2047453 0 7 | 3793637 00:00:04 pgbench pgbench_accounts active Client.~ COPY TO PIPE 1967216 0 196854 2051574 0 8 | 2021/06/14 11:56:36, rate: 1s 9 | 3793641 00:00:04 tmp 47542 active IO.Data~ COPY FROM PIPE 0 244976 2555726 0 10 | 3793637 00:00:05 pgbench pgbench_accounts active Client.~ COPY TO PIPE 1967216 0 245319 2559305 0 11 | 2021/06/14 11:56:37, rate: 1s 12 | 3793641 00:00:05 tmp 47542 active IO.WALS~ COPY FROM PIPE 0 287944 3004686 0 13 | 3793637 00:00:06 pgbench pgbench_accounts active Client.~ COPY TO PIPE 1967216 0 288378 3009226 0 14 | 2021/06/14 11:56:38, rate: 1s 15 | 3793641 00:00:06 tmp 47542 active f COPY FROM PIPE 0 336554 3512607 0 16 | 3793637 00:00:07 pgbench pgbench_accounts active f COPY TO PIPE 1967216 0 336857 3515774 0 17 | 2021/06/14 11:56:39, rate: 1s 18 | 3793641 00:00:08 tmp 47542 active f COPY FROM PIPE 0 381876 3986178 0 19 | 3793637 00:00:08 pgbench pgbench_accounts active f COPY TO PIPE 1967216 0 382184 3989401 0 20 | 2021/06/14 11:56:40, rate: 1s 21 | 3793641 00:00:09 tmp 47542 active LWLock.~ COPY FROM PIPE 0 429562 4484450 0 22 | 3793637 00:00:09 pgbench pgbench_accounts active Client.~ COPY TO PIPE 1967216 0 430023 4489265 0 23 | 2021/06/14 11:56:41, rate: 1s 24 | 3793641 00:00:09 tmp 47542 active IO.WALS~ COPY FROM PIPE 0 476402 4973874 0 25 | 3793637 00:00:10 pgbench pgbench_accounts active Client.~ COPY TO PIPE 1967216 0 476782 4977840 0 26 | 2021/06/14 11:56:42, rate: 1s 27 | 3793641 00:00:10 tmp 47542 active LWLock.~ COPY FROM PIPE 0 518001 5408542 0 28 | 3793637 00:00:11 pgbench pgbench_accounts active Client.~ COPY TO PIPE 1967216 0 518326 5411934 0 29 | -------------------------------------------------------------------------------- /report/testdata/report_progress_index.golden: -------------------------------------------------------------------------------- 1 | pid xact_age datname relation index state waiting phase locker_pid lockers size_total/done,% tuples_total/done,% parts_total/done,% query  2 | 2021/06/14 11:56:34, rate: 1s 3 | 3788780 00:00:04 pgbench pgbench_accounts pgbench_accounts_abalance_idx active f building index: loading tuples ~ 0 0/0 0/0.00 15000000/25.00 0/0.00 create index CONCURRENTLY pgben~ 4 | 2021/06/14 11:56:35, rate: 1s 5 | 3788780 00:00:05 pgbench pgbench_accounts pgbench_accounts_abalance_idx active LWLock.~ building index: loading tuples ~ 0 0/0 0/0.00 15000000/60.00 0/0.00 create index CONCURRENTLY pgben~ 6 | 2021/06/14 11:56:36, rate: 1s 7 | 3788780 00:00:06 pgbench pgbench_accounts pgbench_accounts_abalance_idx active IO.WALS~ building index: loading tuples ~ 0 0/0 0/0.00 15000000/83.00 0/0.00 create index CONCURRENTLY pgben~ 8 | 2021/06/14 11:56:37, rate: 1s 9 | 3788780 00:00:07 pgbench pgbench_accounts pgbench_accounts_abalance_idx active IO.Data~ building index: loading tuples ~ 0 0/0 0/0.00 15000000/100.00 0/0.00 create index CONCURRENTLY pgben~ 10 | 2021/06/14 11:56:38, rate: 1s 11 | 3788780 00:00:08 pgbench pgbench_accounts pgbench_accounts_abalance_idx active f index validation: scanning index 0 0/0 101768/3.00 0/0.00 0/0.00 create index CONCURRENTLY pgben~ 12 | 2021/06/14 11:56:39, rate: 1s 13 | 3788780 00:00:09 pgbench pgbench_accounts pgbench_accounts_abalance_idx active IO.BufF~ index validation: scanning table 0 0/0 3781592/3.00 0/0.00 0/0.00 create index CONCURRENTLY pgben~ 14 | 2021/06/14 11:56:40, rate: 1s 15 | 3788780 00:00:10 pgbench pgbench_accounts pgbench_accounts_abalance_idx active IO.Data~ index validation: scanning table 0 0/0 3781592/44.00 0/0.00 0/0.00 create index CONCURRENTLY pgben~ 16 | -------------------------------------------------------------------------------- /report/testdata/report_progress_vacuum.golden: -------------------------------------------------------------------------------- 1 | pid xact_age datname relation state waiting phase size_total,KiB scanned_total,% vacuumed_total,% scanned,KiB vacuumed,KiB query  2 | 2021/06/14 11:56:34, rate: 1s 3 | 3790850 00:00:27 pgbench pgbench_accounts active IO.WALSync scanning heap 3781592 15.00 0.00 3264 0 autovacuum: VACUUM public.pgben~ 4 | 2021/06/14 11:56:35, rate: 1s 5 | 3790850 00:00:28 pgbench pgbench_accounts active IO.WALSync scanning heap 3781592 15.00 0.00 3248 0 autovacuum: VACUUM public.pgben~ 6 | 2021/06/14 11:56:36, rate: 1s 7 | 3790850 00:00:29 pgbench pgbench_accounts active IO.WALSync scanning heap 3781592 15.00 0.00 3248 0 autovacuum: VACUUM public.pgben~ 8 | 2021/06/14 11:56:37, rate: 1s 9 | 3790850 00:00:30 pgbench pgbench_accounts active IO.WALSync scanning heap 3781592 15.00 0.00 2920 0 autovacuum: VACUUM public.pgben~ 10 | 2021/06/14 11:56:38, rate: 1s 11 | 3790850 00:00:31 pgbench pgbench_accounts active IO.WALSync scanning heap 3781592 15.00 0.00 3280 0 autovacuum: VACUUM public.pgben~ 12 | 2021/06/14 11:56:39, rate: 1s 13 | 3790850 00:00:32 pgbench pgbench_accounts active IO.WALSync scanning heap 3781592 15.00 0.00 2232 0 autovacuum: VACUUM public.pgben~ 14 | 2021/06/14 11:56:40, rate: 1s 15 | 3790850 00:00:33 pgbench pgbench_accounts active Timeout.V~ scanning heap 3781592 16.00 0.00 2576 0 autovacuum: VACUUM public.pgben~ 16 | 2021/06/14 11:56:41, rate: 1s 17 | 3790850 00:00:34 pgbench pgbench_accounts active IO.WALSync scanning heap 3781592 16.00 0.00 2368 0 autovacuum: VACUUM public.pgben~ 18 | 2021/06/14 11:56:42, rate: 1s 19 | 3790850 00:00:35 pgbench pgbench_accounts active IO.WALSync scanning heap 3781592 16.00 0.00 2816 0 autovacuum: VACUUM public.pgben~ 20 | -------------------------------------------------------------------------------- /report/testdata/report_replication.golden: -------------------------------------------------------------------------------- 1 | pid client user name state mode wal,KiB pending,KiB write,KiB flush,KiB replay,KiB total,KiB write flush replay  2 | 2021/06/14 11:56:34, rate: 1s 3 | 2967718 127.0.0.1/32 postgres 14/replica streaming async 3337 0 355 525 3 882 00:00:00 00:00:00 00:00:00 4 | 2021/06/14 11:56:35, rate: 1s 5 | 2967718 127.0.0.1/32 postgres 14/replica streaming async 2921 0 0 362 0 362 00:00:00 00:00:00 00:00:00 6 | 2021/06/14 11:56:36, rate: 1s 7 | 2967718 127.0.0.1/32 postgres 14/replica streaming async 3030 0 228 364 86 678 00:00:00 00:00:00 00:00:00 8 | 2021/06/14 11:56:37, rate: 1s 9 | 2967718 127.0.0.1/32 postgres 14/replica streaming async 1774 0 0 230 198 427 00:00:00 00:00:00 00:00:00 10 | 2021/06/14 11:56:38, rate: 1s 11 | 2967718 127.0.0.1/32 postgres 14/replica streaming async 2353 0 0 488 0 488 00:00:00 00:00:00 00:00:00 12 | 2021/06/14 11:56:39, rate: 1s 13 | 2967718 127.0.0.1/32 postgres 14/replica streaming async 1909 0 372 291 16 679 00:00:00 00:00:00 00:00:00 14 | 2021/06/14 11:56:40, rate: 1s 15 | 2967718 127.0.0.1/32 postgres 14/replica streaming async 1738 0 55 63 16 134 00:00:00 00:00:00 00:00:00 16 | 2021/06/14 11:56:41, rate: 1s 17 | 2967718 127.0.0.1/32 postgres 14/replica streaming async 1802 0 0 213 0 212 00:00:00 00:00:00 00:00:00 18 | 2021/06/14 11:56:42, rate: 1s 19 | 2967718 127.0.0.1/32 postgres 14/replica streaming async 1954 0 87 136 80 302 00:00:00 00:00:00 00:00:00 20 | -------------------------------------------------------------------------------- /report/testdata/report_sample.golden: -------------------------------------------------------------------------------- 1 | datname backends commits rollbacks reads hits returned fetched inserts updates deletes conflicts deadlocks csum_fails temp_files temp_bytes read_t write_t stats_age  2 | 2021/01/01 00:00:01, rate: 1s 3 | example_db 11 500 5 2000 10000 1000 3000 4000 6000 1500 25 30 1 50 25000 250 3 11 days 10:10:11 4 | -------------------------------------------------------------------------------- /report/testdata/report_statements_timings_limit.golden: -------------------------------------------------------------------------------- 1 | user database all_total read_total write_total exec_total all,ms read,ms write,ms exec,ms calls queryid query  2 | 2021/06/14 11:56:42, rate: 1s 3 | postgres postgres 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 8820b43fce SELECT current_setting($1),pg_c~ 4 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 f844975654 insert into pgbench_branches(bi~ 5 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 c9a1f648ed insert into pgbench_branches(bi~ 6 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 cab2e6e673 insert into pgbench_tellers(tid~ 7 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 5f5fd4b0e3 insert into pgbench_tellers(tid~ 8 | postgres pgscv_fixtures 00:01:11 00:00:00 00:00:00 00:01:11 0 0 0 0 0 7a4ca00981 SELECT current_database() AS da~ 9 | postgres pgscv_fixtures 00:00:32 00:00:00 00:00:00 00:00:32 0 0 0 0 0 e8ac4a8798 SELECT sum(pg_total_relation_si~ 10 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 cc9b0d1c16 insert into pgbench_branches(bi~ 11 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 4f36e29390 insert into pgbench_branches(bi~ 12 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 1c95078779 insert into pgbench_branches(bi~ 13 | -------------------------------------------------------------------------------- /report/testdata/report_statements_timings_limit_truncate.golden: -------------------------------------------------------------------------------- 1 | user database all_total read_total write_total exec_total all,ms read,ms write,ms exec,ms calls queryid query  2 | 2021/06/14 11:56:42, rate: 1s 3 | postgres postgres 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 8820b43fce SELECT current_setting($1),pg_current_logfile() 4 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 f844975654 insert into pgbench_branches(bid,bbalance) values($1,$2) 5 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 c9a1f648ed insert into pgbench_branches(bid,bbalance) values($1,$2) 6 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 cab2e6e673 insert into pgbench_tellers(tid,bid,tbalance) values ($1,$2,$3) 7 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 5f5fd4b0e3 insert into pgbench_tellers(tid,bid,tbalance) values ($1,$2,$3) 8 | postgres pgscv_fixtures 00:01:11 00:00:00 00:00:00 00:01:11 0 0 0 0 0 7a4ca00981 SELECT current_database() AS database, s1.schemaname AS schema,~ 9 | postgres pgscv_fixtures 00:00:32 00:00:00 00:00:00 00:00:32 0 0 0 0 0 e8ac4a8798 SELECT sum(pg_total_relation_size(relname::regclass)) AS bytes ~ 10 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 cc9b0d1c16 insert into pgbench_branches(bid,bbalance) values($1,$2) 11 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 4f36e29390 insert into pgbench_branches(bid,bbalance) values($1,$2) 12 | postgres pgbench 00:00:00 00:00:00 00:00:00 00:00:00 0 0 0 0 0 1c95078779 insert into pgbench_branches(bid,bbalance) values($1,$2) 13 | -------------------------------------------------------------------------------- /report/testdata/report_wal.golden: -------------------------------------------------------------------------------- 1 | source waldir_size wal,KiB records fpi write sync write,ms sync,ms buffers_full stats_age  2 | 2021/06/14 11:56:34, rate: 1s 3 | WAL 1040 MB 2853.63 691 363 9 9 2.54 814.71 0 9 days 23:12:03 4 | 2021/06/14 11:56:35, rate: 1s 5 | WAL 1040 MB 3778.52 916 481 15 14 1.63 1032.14 0 9 days 23:12:04 6 | 2021/06/14 11:56:36, rate: 1s 7 | WAL 1040 MB 2664.72 675 339 10 9 2.16 850.71 0 9 days 23:12:05 8 | 2021/06/14 11:56:37, rate: 1s 9 | WAL 1040 MB 2300.94 582 293 9 9 1.29 1054.45 0 9 days 23:12:06 10 | 2021/06/14 11:56:38, rate: 1s 11 | WAL 1040 MB 2017.38 559 256 11 10 1.07 1013.44 0 9 days 23:12:07 12 | 2021/06/14 11:56:39, rate: 1s 13 | WAL 1040 MB 1855.27 513 236 8 8 1.10 1065.70 0 9 days 23:12:08 14 | 2021/06/14 11:56:40, rate: 1s 15 | WAL 1040 MB 1128.37 381 143 11 11 3.00 1043.58 0 9 days 23:12:09 16 | 2021/06/14 11:56:41, rate: 1s 17 | WAL 1040 MB 1981.06 530 252 8 8 0.52 659.25 0 9 days 23:12:10 18 | 2021/06/14 11:56:42, rate: 1s 19 | WAL 1040 MB 1712.58 586 217 13 12 1.30 1336.60 0 9 days 23:12:11 20 | -------------------------------------------------------------------------------- /testing/Dockerfile: -------------------------------------------------------------------------------- 1 | # lesovsky/pgcenter-testing 2 | # __release_tag__ postgres 14.1 was released 2021-11-11 3 | # __release_tag__ postgres 13.5 was released 2021-11-11 4 | # __release_tag__ postgres 12.9 was released 2021-11-11 5 | # __release_tag__ postgres 11.14 was released 2021-11-11 6 | # __release_tag__ postgres 10.19 was released 2021-11-11 7 | # __release_tag__ postgres 9.6.24 was released 2021-11-11 8 | # __release_tag__ postgres 9.5.24 was released 2020-12-03 -- EOL 9 | # __release_tag__ golang 1.17.6 was released 2022-01-06 10 | # __release_tag__ golangci-lint v1.44.0 was released 2022-01-25 11 | # __release_tag__ gosec v2.9.6 was released 2022-01-10 12 | FROM ubuntu:20.04 13 | 14 | LABEL version="0.0.7" 15 | 16 | ENV DEBIAN_FRONTEND=noninteractive 17 | 18 | # install dependencies 19 | RUN apt-get update && \ 20 | apt-get install -y locales curl ca-certificates gnupg make gcc git && \ 21 | sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && \ 22 | locale-gen && \ 23 | curl -s https://www.postgresql.org/media/keys/ACCC4CF8.asc -o /tmp/ACCC4CF8.asc && \ 24 | apt-key add /tmp/ACCC4CF8.asc && \ 25 | echo "deb http://apt.postgresql.org/pub/repos/apt focal-pgdg main 14" > /etc/apt/sources.list.d/pgdg.list && \ 26 | apt-get update && \ 27 | apt-get install -y postgresql-9.5 postgresql-9.6 postgresql-10 postgresql-11 postgresql-12 postgresql-13 postgresql-14 \ 28 | postgresql-plperl-9.5 postgresql-plperl-9.6 postgresql-plperl-10 postgresql-plperl-11 postgresql-plperl-12 postgresql-plperl-13 postgresql-plperl-14\ 29 | libfilesys-df-perl && \ 30 | cpan Module::Build && \ 31 | cpan Linux::Ethtool::Settings && \ 32 | curl -s -L https://go.dev/dl/go1.17.6.linux-amd64.tar.gz -o - | \ 33 | tar xzf - -C /usr/local && \ 34 | cp /usr/local/go/bin/go /usr/local/bin/ && \ 35 | curl -s -L https://github.com/golangci/golangci-lint/releases/download/v1.44.0/golangci-lint-1.44.0-linux-amd64.tar.gz -o - | \ 36 | tar xzf - -C /usr/local golangci-lint-1.44.0-linux-amd64/golangci-lint && \ 37 | cp /usr/local/golangci-lint-1.44.0-linux-amd64/golangci-lint /usr/local/bin/ && \ 38 | curl -s -L https://github.com/securego/gosec/releases/download/v2.9.6/gosec_2.9.6_linux_amd64.tar.gz -o - | \ 39 | tar xzf - -C /usr/local/bin gosec && \ 40 | mkdir /usr/local/testing/ && \ 41 | rm -rf /var/lib/apt/lists/* 42 | 43 | # copy prepare test environment scripts 44 | COPY prepare-test-environment.sh /usr/local/bin/ 45 | COPY fixtures.sql /usr/local/testing/ 46 | 47 | CMD ["echo", "I'm pgcenter-testing 0.0.7"] 48 | -------------------------------------------------------------------------------- /testing/README.md: -------------------------------------------------------------------------------- 1 | ## Testing 2 | 3 | This stuff is related to testing on local development. 4 | 5 | - Dockerfile used for making `lesovsky/pgcenter-testing` Docker image. This image contains all necessary Postgres versions and setup scripts. Container started from the image is used for running local tests. 6 | - fixtures.sql - SQL-script which creates all necessary stuff in testing environment 7 | - prepare-test-environment.sh - Shell-script which setup testing environment in a container (prepare configs, start services) 8 | - e2e.sh - Shell-script used in CI for testing high-level functionality. 9 | 10 | See these [docs](../doc/development.md) for more info about local development. -------------------------------------------------------------------------------- /testing/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # E2E tests for pgcenter. 5 | # 6 | 7 | set -euxo pipefail 8 | 9 | rm -rf /tmp/pgcenter-e2e 10 | mkdir /tmp/pgcenter-e2e 11 | 12 | # 13 | # pgcenter record 14 | # 15 | for port in 21995 21996 21910 21911 21912 21913 21914; do 16 | pgcenter record -i 1s -c 5 -h 127.0.0.1 -p $port -U postgres -d pgcenter_fixtures -f /tmp/pgcenter-e2e/pgcenter.stat.$port.tar 17 | done 18 | 19 | # 20 | # pgcenter report 21 | # 22 | for port in 21995 21996 21910 21911 21912 21913 21914; do 23 | for arg in -A -R -D -T -I -S -F -Xm -Xg -Xi -Xt -Xl -Xw -Pv -Pc -Pi -Pa -Pb -Pz; do 24 | pgcenter report $arg -f /tmp/pgcenter-e2e/pgcenter.stat.$port.tar > /tmp/pgcenter-e2e/pgcenter.stat.$port.$arg.out 25 | done 26 | done 27 | -------------------------------------------------------------------------------- /testing/prepare-test-environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # copy configuration files into data directories 4 | for v in 9.5 9.6 10 11 12 13 14; do 5 | su - postgres -c "mv /etc/postgresql/${v}/main/postgresql.conf /var/lib/postgresql/${v}/main/" 6 | done 7 | 8 | # add extra configuration parameters 9 | for v in 9.5 9.6 10 11 12 13 14; do 10 | port="219$(echo $v |tr -d .)" 11 | { 12 | echo "listen_addresses = '*' 13 | port = $port 14 | shared_buffers = 16MB 15 | ssl = on 16 | ssl_cert_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem' 17 | ssl_key_file = '/etc/ssl/private/ssl-cert-snakeoil.key' 18 | logging_collector = on 19 | log_directory = '/var/log/postgresql' 20 | log_filename = 'postgresql-$v.log' 21 | track_io_timing = on 22 | track_functions = all 23 | shared_preload_libraries = 'pg_stat_statements'" 24 | } >> /var/lib/postgresql/${v}/main/postgresql.auto.conf 25 | 26 | { 27 | echo "local all all trust 28 | host all all 0.0.0.0/0 trust" 29 | } > /etc/postgresql/${v}/main/pg_hba.conf 30 | 31 | mkdir /var/lib/postgresql/${v}/main/conf.d 32 | done 33 | 34 | # run main postgres 35 | for v in 9.5 9.6 10 11 12 13 14; do 36 | su - postgres -c "/usr/lib/postgresql/${v}/bin/pg_ctl -w -t 30 -l /var/log/postgresql/startup-${v}.log -D /var/lib/postgresql/${v}/main start" 37 | done 38 | 39 | # install pgcenter schema 40 | for v in 9.5 9.6 10 11 12 13 14; do 41 | port="219$(echo $v |tr -d .)" 42 | su - postgres -c "psql -p $port -f /usr/local/testing/fixtures.sql" 43 | done 44 | 45 | # check services availability 46 | for v in 9.5 9.6 10 11 12 13 14; do 47 | port="219$(echo $v |tr -d .)" 48 | pg_isready -t 10 -h 127.0.0.1 -p "$port" -U postgres -d pgcenter_fixtures 49 | done 50 | -------------------------------------------------------------------------------- /top/config.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/query" 5 | "github.com/lesovsky/pgcenter/internal/stat" 6 | "github.com/lesovsky/pgcenter/internal/view" 7 | ) 8 | 9 | // config defines 'top' program runtime configuration. 10 | type config struct { 11 | view view.View // Current active view. 12 | views view.Views // List of all available views. 13 | queryOptions query.Options // Queries' settings that might depend on Postgres version. 14 | viewCh chan view.View // Channel used for passing view settings to stats goroutine. 15 | logtail stat.Logfile // Logfile used for working with Postgres log file. 16 | dialog dialogType // Remember current user-started dialog, used for selecting needed dialog handler. 17 | menu menuStyle // When working with menus, keep properties of the menu. 18 | procMask int // Process mask used for selecting group of process. 19 | } 20 | 21 | // newConfig creates 'top' initial configuration. 22 | func newConfig() *config { 23 | views := view.New() 24 | 25 | return &config{ 26 | views: views, 27 | viewCh: make(chan view.View), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /top/config_test.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func Test_newConfig(t *testing.T) { 9 | c := newConfig() 10 | assert.Greater(t, len(c.views), 0) 11 | } 12 | -------------------------------------------------------------------------------- /top/errrate.go: -------------------------------------------------------------------------------- 1 | // Error rate facility used with gocui.MainLoop to check errors rate in case if main loop fails too often. 2 | // In case of seldom error or in case when gocui.MainLoop stopped manually (when running 3-rd party programs) this 3 | // seldom errors can be ignored and gocui.MainLoop can be restarted. But if something really goes wrong inside gocui, 4 | // error rate facility should catch this situation and stop program execution. 5 | 6 | package top 7 | 8 | import ( 9 | "fmt" 10 | "time" 11 | ) 12 | 13 | // errorRate describes details about occurred errors. 14 | type errorRate struct { 15 | timeCurr time.Time // time of the latest error 16 | timePrev time.Time // time of previous error occurred 17 | timeElapsed time.Duration // interval between two last errors 18 | errCnt int // errors counter 19 | } 20 | 21 | // check method checks number of errors occurred within specified interval. 22 | func (e *errorRate) check(errInterval time.Duration, errMaxcount int) error { 23 | e.timeCurr = time.Now() 24 | e.timeElapsed = e.timeCurr.Sub(e.timePrev) 25 | 26 | if e.timeElapsed > errInterval { // interval between errors too long, reset counter 27 | e.timePrev = e.timeCurr 28 | e.errCnt = 0 29 | } else { // otherwise increment counter 30 | e.errCnt++ 31 | if e.errCnt > errMaxcount { // if errors limit is reached, exit with error 32 | return fmt.Errorf("too many errors") 33 | } 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /top/errrate_test.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func Test_errorRate_check(t *testing.T) { 10 | testcases := []struct { 11 | erate errorRate 12 | want int 13 | ok bool 14 | }{ 15 | {erate: errorRate{timePrev: time.Now().Add(time.Duration(-10) * time.Second), errCnt: 0}, ok: true, want: 1}, 16 | {erate: errorRate{timePrev: time.Now().Add(time.Duration(-70) * time.Second), errCnt: 5}, ok: true, want: 0}, 17 | {erate: errorRate{timePrev: time.Now().Add(time.Duration(-10) * time.Second), errCnt: 9}, ok: true, want: 10}, 18 | {erate: errorRate{timePrev: time.Now().Add(time.Duration(-10) * time.Second), errCnt: 10}, ok: false, want: 0}, 19 | } 20 | 21 | for _, tc := range testcases { 22 | if tc.ok { 23 | assert.NoError(t, tc.erate.check(time.Minute, 10)) 24 | assert.Equal(t, tc.erate.errCnt, tc.want) 25 | } else { 26 | assert.Error(t, tc.erate.check(time.Minute, 10)) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /top/extra.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jroimartin/gocui" 6 | "github.com/lesovsky/pgcenter/internal/stat" 7 | "os" 8 | ) 9 | 10 | // showExtra manages displaying extra stats - depending on user selection it opens or closes dedicated 'view' for extra stats. 11 | func showExtra(app *app, extra int) func(g *gocui.Gui, v *gocui.View) error { 12 | return func(g *gocui.Gui, v *gocui.View) error { 13 | // Close 'view' if passed type of extra stats are already displayed 14 | if app.config.view.ShowExtra == extra { 15 | if extra == stat.CollectLogtail { 16 | err := app.config.logtail.Close() 17 | if err != nil { 18 | return err 19 | } 20 | } 21 | 22 | return closeExtraView(g, v, app.config) 23 | } 24 | 25 | var msg string 26 | 27 | // Depending on requested extra stats, additional steps might to be necessary. 28 | switch extra { 29 | case stat.CollectDiskstats: 30 | msg = "Show block devices statistics" 31 | case stat.CollectNetdev: 32 | msg = "Show network interfaces statistics" 33 | case stat.CollectFsstats: 34 | msg = "Show mounted filesystems statistics" 35 | case stat.CollectLogtail: 36 | if !app.db.Local { 37 | printCmdline(g, "Log tail is not supported for remote hosts") 38 | return nil 39 | } 40 | 41 | logfile, err := stat.GetPostgresCurrentLogfile(app.db, app.postgresProps.VersionNum) 42 | if err != nil { 43 | return err 44 | } 45 | app.config.logtail.Path = logfile 46 | app.config.logtail.Size = 0 47 | 48 | // Check the logfile exists, is not empty and available for reading. 49 | if info, err := os.Stat(app.config.logtail.Path); err == nil && info.Size() == 0 { 50 | printCmdline(g, "Empty logfile") 51 | return nil 52 | } else if err != nil { 53 | printCmdline(g, "Failed to stat logfile: %s", err) 54 | return nil 55 | } 56 | if err := app.config.logtail.Open(); err != nil { 57 | printCmdline(g, "Failed to open %s", app.config.logtail.Path) 58 | return nil 59 | } 60 | 61 | msg = "Tail Postgres log" 62 | } 63 | 64 | // If other type of extra stats already displayed, ignore it and reopen 'view' for requested extra stats. 65 | if err := openExtraView(g, v); err != nil { 66 | return err 67 | } 68 | 69 | // Update views configuration and notify stats goroutine - it have to start collecting extra stats. 70 | for k, v := range app.config.views { 71 | v.ShowExtra = extra 72 | app.config.views[k] = v 73 | } 74 | app.config.view.ShowExtra = extra 75 | app.config.viewCh <- app.config.view 76 | 77 | printCmdline(g, msg) 78 | 79 | return nil 80 | } 81 | } 82 | 83 | // openExtraView create new UI view object for displaying extra stats. 84 | func openExtraView(g *gocui.Gui, _ *gocui.View) error { 85 | maxX, maxY := g.Size() 86 | v, err := g.SetView("extra", -1, 3*maxY/5-1, maxX-1, maxY-1) 87 | if err != nil { 88 | // gocui.ErrUnknownView is OK, it means a new view has been created. 89 | if err != gocui.ErrUnknownView { 90 | return fmt.Errorf("set extra view on layout failed: %s", err) 91 | } 92 | } 93 | v.Frame = false 94 | return nil 95 | } 96 | 97 | // closeExtraView updates configuration and closes view with extra stats. 98 | func closeExtraView(g *gocui.Gui, _ *gocui.View, c *config) error { 99 | for k, v := range c.views { 100 | v.ShowExtra = stat.CollectNone 101 | c.views[k] = v 102 | } 103 | c.view.ShowExtra = stat.CollectNone 104 | c.viewCh <- c.view 105 | 106 | return g.DeleteView("extra") 107 | } 108 | -------------------------------------------------------------------------------- /top/help.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jroimartin/gocui" 6 | "github.com/lesovsky/pgcenter/internal/version" 7 | ) 8 | 9 | const ( 10 | helpTemplate = `%s: Help for interactive commands 11 | 12 | general actions: 13 | a,f,r,w mode: 'a' activity, 'f' functions, 'r' replication, 'w' WAL, 14 | s,t,i 's' tables sizes, 't' tables, 'i' indexes. 15 | d,D 'd' pg_stat_database switch, 'D' pg_stat_database menu. 16 | x,X 'x' pg_stat_statements switch, 'X' pg_stat_statements menu. 17 | p,P 'p' pg_stat_progress_* switch, 'P' pg_stat_progress_* menu. 18 | Left,Right,<,/ 'Left,Right' change column sort, '<' desc/asc sort toggle, '/' set filter. 19 | Up,Down 'Up' increase column width, 'Down' decrease column width. 20 | C,E,R config: 'C' show config, 'E' edit configs, 'R' reload config. 21 | ~ start psql session. 22 | l open log file with pager. 23 | 24 | extra stats actions: 25 | B,N,F,L 'B' diskstat, 'N' nicstat, 'F' filesystems, L' logtail. 26 | 27 | activity actions: 28 | -,_ '-' cancel backend by pid, '_' terminate backend by pid. 29 | n,m 'n' set new mask, 'm' show current mask. 30 | k,K 'k' cancel group of queries using mask, 'K' terminate group of backends using mask. 31 | I show IDLE connections toggle. 32 | A change activity age threshold. 33 | G get query report. 34 | 35 | other actions: 36 | , Q ',' show system tables on/off, 'Q' reset postgresql statistics counters. 37 | z 'z' set refresh interval. 38 | h,F1 show this tab. 39 | q,Ctrl+Q quit. 40 | 41 | Type 'q' or 'Esc' to continue.` 42 | ) 43 | 44 | // showHelp opens fullscreen view with built-in help. 45 | func showHelp(g *gocui.Gui, _ *gocui.View) error { 46 | maxX, maxY := g.Size() 47 | if v, err := g.SetView("help", -1, -1, maxX-1, maxY-1); err != nil { 48 | if err != gocui.ErrUnknownView { 49 | return fmt.Errorf("set 'help' view on layout failed: %s", err) 50 | } 51 | 52 | name, tag, commit, branch := version.Version() 53 | versionStr := fmt.Sprintf("%s %s (%s, %s)", name, tag, commit, branch) 54 | 55 | v.Frame = false 56 | _, err = fmt.Fprintf(v, helpTemplate, versionStr) 57 | if err != nil { 58 | return fmt.Errorf("print on 'help' view failed: %s", err) 59 | } 60 | 61 | if _, err := g.SetCurrentView("help"); err != nil { 62 | return fmt.Errorf("set 'help' view as current on layout failed: %s", err) 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | // closeHelp closes 'help' view and switches focus to 'sysstat' view. 69 | func closeHelp(g *gocui.Gui, v *gocui.View) error { 70 | v.Clear() 71 | err := g.DeleteView("help") 72 | if err != nil { 73 | return fmt.Errorf("delete help view failed: %s", err) 74 | } 75 | 76 | if _, err := g.SetCurrentView("sysstat"); err != nil { 77 | return fmt.Errorf("set focus on sysstat view failed: %s", err) 78 | } 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /top/keybindings.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jroimartin/gocui" 6 | "github.com/lesovsky/pgcenter/internal/stat" 7 | ) 8 | 9 | // Key represents binding between key button and handler should be running when user presses the button. 10 | type key struct { 11 | viewname string 12 | key interface{} 13 | handler func(g *gocui.Gui, v *gocui.View) error 14 | } 15 | 16 | // keybindings set up key bindings with handlers. 17 | func keybindings(app *app) error { 18 | var keys = []key{ 19 | {"", gocui.KeyCtrlC, app.quit()}, 20 | {"", gocui.KeyCtrlQ, app.quit()}, 21 | {"sysstat", 'q', app.quit()}, 22 | {"sysstat", gocui.KeyArrowLeft, orderKeyLeft(app.config)}, 23 | {"sysstat", gocui.KeyArrowRight, orderKeyRight(app.config)}, 24 | {"sysstat", gocui.KeyArrowUp, increaseWidth(app.config)}, 25 | {"sysstat", gocui.KeyArrowDown, decreaseWidth(app.config)}, 26 | {"sysstat", '<', switchSortOrder(app.config)}, 27 | {"sysstat", ',', toggleSysTables(app.config)}, 28 | {"sysstat", 'I', toggleIdleConns(app.config)}, 29 | {"sysstat", 'd', switchViewTo(app, "databases")}, 30 | {"sysstat", 'r', switchViewTo(app, "replication")}, 31 | {"sysstat", 't', switchViewTo(app, "tables")}, 32 | {"sysstat", 'i', switchViewTo(app, "indexes")}, 33 | {"sysstat", 's', switchViewTo(app, "sizes")}, 34 | {"sysstat", 'f', switchViewTo(app, "functions")}, 35 | {"sysstat", 'w', switchViewTo(app, "wal")}, 36 | {"sysstat", 'p', switchViewTo(app, "progress")}, 37 | {"sysstat", 'a', switchViewTo(app, "activity")}, 38 | {"sysstat", 'x', switchViewTo(app, "statements")}, 39 | {"sysstat", 'Q', resetStat(app.db, app.postgresProps.ExtPGSSSchema)}, 40 | {"sysstat", 'E', menuOpen(menuConf, app.config, "")}, 41 | {"sysstat", 'D', menuOpen(menuDatabases, app.config, "")}, 42 | {"sysstat", 'X', menuOpen(menuPgss, app.config, app.postgresProps.ExtPGSSSchema)}, 43 | {"sysstat", 'P', menuOpen(menuProgress, app.config, "")}, 44 | {"sysstat", 'l', showPgLog(app.db, app.postgresProps.VersionNum, app.uiExit)}, 45 | {"sysstat", 'C', showPgConfig(app.db, app.uiExit)}, 46 | {"sysstat", '~', runPsql(app.db, app.uiExit)}, 47 | {"sysstat", 'B', showExtra(app, stat.CollectDiskstats)}, 48 | {"sysstat", 'N', showExtra(app, stat.CollectNetdev)}, 49 | {"sysstat", 'F', showExtra(app, stat.CollectFsstats)}, 50 | {"sysstat", 'L', showExtra(app, stat.CollectLogtail)}, 51 | {"sysstat", 'R', dialogOpen(app, dialogPgReload)}, 52 | {"sysstat", '/', dialogOpen(app, dialogFilter)}, 53 | {"sysstat", '-', dialogOpen(app, dialogCancelQuery)}, 54 | {"sysstat", '_', dialogOpen(app, dialogTerminateBackend)}, 55 | {"sysstat", 'n', dialogOpen(app, dialogSetMask)}, 56 | {"sysstat", 'm', showProcMask(app.config)}, 57 | {"sysstat", 'k', dialogOpen(app, dialogCancelGroup)}, 58 | {"sysstat", 'K', dialogOpen(app, dialogTerminateGroup)}, 59 | {"sysstat", 'A', dialogOpen(app, dialogChangeAge)}, 60 | {"sysstat", 'G', dialogOpen(app, dialogQueryReport)}, 61 | {"sysstat", 'z', dialogOpen(app, dialogChangeRefresh)}, 62 | {"dialog", gocui.KeyEsc, dialogCancel(app)}, 63 | {"dialog", gocui.KeyEnter, dialogFinish(app)}, 64 | {"menu", gocui.KeyEsc, menuClose}, 65 | {"menu", gocui.KeyArrowUp, moveCursor(moveUp, app.config)}, 66 | {"menu", gocui.KeyArrowDown, moveCursor(moveDown, app.config)}, 67 | {"menu", gocui.KeyEnter, menuSelect(app)}, 68 | {"sysstat", 'h', showHelp}, 69 | {"sysstat", gocui.KeyF1, showHelp}, 70 | {"help", gocui.KeyEsc, closeHelp}, 71 | {"help", 'q', closeHelp}, 72 | } 73 | 74 | app.ui.InputEsc = true 75 | 76 | for _, k := range keys { 77 | if err := app.ui.SetKeybinding(k.viewname, k.key, gocui.ModNone, k.handler); err != nil { 78 | return fmt.Errorf("setup keybindings failed: %s", err) 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /top/menu_test.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func Test_selectMenuStyle(t *testing.T) { 9 | testcases := []struct { 10 | menu menuType 11 | want int 12 | }{ 13 | {menu: menuNone, want: 0}, 14 | {menu: menuDatabases, want: 2}, 15 | {menu: menuPgss, want: 6}, 16 | {menu: menuProgress, want: 6}, 17 | {menu: menuConf, want: 4}, 18 | } 19 | 20 | for _, tc := range testcases { 21 | got := selectMenuStyle(tc.menu) 22 | assert.Equal(t, tc.want, len(got.items)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /top/pgconfig.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/jroimartin/gocui" 7 | "github.com/lesovsky/pgcenter/internal/postgres" 8 | "github.com/lesovsky/pgcenter/internal/query" 9 | "github.com/lesovsky/pgcenter/internal/stat" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | ) 14 | 15 | const ( 16 | // gucMainConfFile is the name of GUC which stores Postgres config file location 17 | gucMainConfFile = "config_file" 18 | // gucHbaFile is the name of GUC which stores Postgres HBA file location 19 | gucHbaFile = "hba_file" 20 | // gucIdentFile is the name of GUC which stores ident file location 21 | gucIdentFile = "ident_file" 22 | // gucRecoveryFile is the name of pseudo-GUC which stores recovery settings location 23 | gucRecoveryFile = "recovery.conf" 24 | // gucDataDir is the name of GUC which stores data directory location 25 | gucDataDir = "data_directory" 26 | ) 27 | 28 | // showPgConfig fetches Postgres configuration settings and opens it in $PAGER program. 29 | func showPgConfig(db *postgres.DB, uiExit chan int) func(g *gocui.Gui, _ *gocui.View) error { 30 | return func(g *gocui.Gui, _ *gocui.View) error { 31 | res, err := stat.NewPGresultQuery(db, query.GetAllSettings) 32 | if err != nil { 33 | printCmdline(g, err.Error()) 34 | return nil 35 | } 36 | 37 | var buf bytes.Buffer 38 | if _, err := fmt.Fprintf(&buf, "PostgreSQL configuration:\n"); err != nil { 39 | printCmdline(g, "print string to buffer failed: %s", err) 40 | return nil 41 | } 42 | 43 | if err := res.Fprint(&buf); err != nil { 44 | printCmdline(g, "print string to buffer failed: %s", err) 45 | return nil 46 | } 47 | 48 | var pager string 49 | if pager = os.Getenv("PAGER"); pager == "" { 50 | pager = "less" 51 | } 52 | 53 | // Exit from UI and stats loop... will restore it after $PAGER is closed. 54 | uiExit <- 1 55 | g.Close() 56 | 57 | cmd := exec.Command(pager) // #nosec G204 58 | cmd.Stdin = strings.NewReader(buf.String()) 59 | cmd.Stdout = os.Stdout 60 | 61 | if err := cmd.Run(); err != nil { 62 | return fmt.Errorf("run pager failed: %s", err) 63 | } 64 | 65 | return nil 66 | } 67 | } 68 | 69 | // editPgConfig opens specified configuration file in $EDITOR program. 70 | func editPgConfig(g *gocui.Gui, db *postgres.DB, filename string, uiExit chan int) error { 71 | if !db.Local { 72 | printCmdline(g, "Edit config is not supported for remote hosts") 73 | return nil 74 | } 75 | 76 | var configFile string 77 | if filename != gucRecoveryFile { 78 | if err := db.QueryRow(query.GetSetting, filename).Scan(&configFile); err != nil { 79 | printCmdline(g, "scan failed: %s", err) 80 | return nil 81 | } 82 | } else { 83 | var dataDirectory string 84 | if err := db.QueryRow(query.GetSetting, gucDataDir).Scan(&dataDirectory); err != nil { 85 | printCmdline(g, "scan failed: %s", err) 86 | return nil 87 | } 88 | configFile = dataDirectory + "/" + filename 89 | } 90 | 91 | var editor string 92 | if editor = os.Getenv("EDITOR"); editor == "" { 93 | editor = "vi" 94 | } 95 | 96 | // Exit from UI and stats loop... will restore it after $EDITOR is closed. 97 | uiExit <- 1 98 | g.Close() 99 | 100 | cmd := exec.Command(editor, configFile) // #nosec G204 101 | cmd.Stdin = os.Stdin 102 | cmd.Stdout = os.Stdout 103 | 104 | if err := cmd.Run(); err != nil { 105 | return fmt.Errorf("run editor failed: %s", err) 106 | } 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /top/pglog.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jroimartin/gocui" 6 | "github.com/lesovsky/pgcenter/internal/postgres" 7 | "github.com/lesovsky/pgcenter/internal/stat" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | // showPgLog opens Postgres log in $PAGER program. 13 | func showPgLog(db *postgres.DB, version int, uiExit chan int) func(g *gocui.Gui, _ *gocui.View) error { 14 | return func(g *gocui.Gui, _ *gocui.View) error { 15 | if !db.Local { 16 | printCmdline(g, "Show log is not supported for remote hosts") 17 | return nil 18 | } 19 | 20 | logfile, err := stat.GetPostgresCurrentLogfile(db, version) 21 | if err != nil { 22 | printCmdline(g, "Can't get path to log file") 23 | return nil 24 | } 25 | 26 | var pager string 27 | if pager = os.Getenv("PAGER"); pager == "" { 28 | pager = "less" 29 | } 30 | 31 | // Exit from UI and stats loop. Restore it after $PAGER is closed. 32 | uiExit <- 1 33 | g.Close() 34 | 35 | cmd := exec.Command(pager, logfile) // #nosec G204 36 | cmd.Stdout = os.Stdout 37 | 38 | if err := cmd.Run(); err != nil { 39 | return fmt.Errorf("open %s failed: %s", logfile, err) 40 | } 41 | 42 | return nil 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /top/psql.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jroimartin/gocui" 6 | "github.com/lesovsky/pgcenter/internal/postgres" 7 | "os" 8 | "os/exec" 9 | "os/signal" 10 | "strconv" 11 | ) 12 | 13 | // runPsql starts psql session to the current connected database. 14 | func runPsql(db *postgres.DB, uiExit chan int) func(g *gocui.Gui, _ *gocui.View) error { 15 | return func(g *gocui.Gui, _ *gocui.View) error { 16 | // Ignore interrupts in pgCenter, because Ctrl+C in psql interrupts pgCenter. 17 | signal.Ignore(os.Interrupt) 18 | 19 | // exit from UI and stats loop... will restore it after psql is closed. 20 | uiExit <- 1 21 | g.Close() 22 | 23 | cfg := db.Config.Config 24 | 25 | cmd := exec.Command( 26 | "psql", 27 | "-h", cfg.Host, 28 | "-p", strconv.Itoa(int(cfg.Port)), 29 | "-U", cfg.User, 30 | "-d", cfg.Database, 31 | ) // #nosec G204 32 | 33 | cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr 34 | 35 | if err := cmd.Run(); err != nil { 36 | return fmt.Errorf("run psql failed: %s", err) 37 | } 38 | 39 | return nil 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /top/reload.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "github.com/lesovsky/pgcenter/internal/postgres" 7 | "github.com/lesovsky/pgcenter/internal/query" 8 | ) 9 | 10 | // doReload performs reload of Postgres service by executing pg_reload_conf(). 11 | func doReload(answer string, db *postgres.DB) string { 12 | var message string 13 | 14 | switch answer { 15 | case "y": 16 | var status sql.NullBool 17 | 18 | err := db.QueryRow(query.ExecReloadConf).Scan(&status) 19 | if err != nil { 20 | message = fmt.Sprintf("Reload: failed, %s", err.Error()) 21 | return message 22 | } 23 | 24 | if status.Bool { 25 | message = "Reload: successful" 26 | } else { 27 | message = "Reload: no error, got NULL response" 28 | } 29 | case "n": 30 | message = "Reload: do nothing, canceled" 31 | default: 32 | message = "Reload: do nothing, invalid input" 33 | } 34 | 35 | return message 36 | } 37 | -------------------------------------------------------------------------------- /top/reload_test.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_doReload(t *testing.T) { 10 | testcases := []struct { 11 | answer string 12 | want string 13 | }{ 14 | {answer: "y", want: "Reload: successful"}, 15 | {answer: "n", want: "Reload: do nothing, canceled"}, 16 | {answer: "q", want: "Reload: do nothing, invalid input"}, 17 | } 18 | 19 | conn, err := postgres.NewTestConnect() 20 | assert.NoError(t, err) 21 | 22 | for _, tc := range testcases { 23 | assert.Equal(t, tc.want, doReload(tc.answer, conn)) 24 | } 25 | 26 | // Test with closed conn 27 | conn.Close() 28 | assert.Equal(t, "Reload: failed, conn closed", doReload(testcases[0].answer, conn)) 29 | } 30 | -------------------------------------------------------------------------------- /top/report_test.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_getQueryReport(t *testing.T) { 10 | // Connect to Postgres and get random queryid needed for testcase. 11 | conn, err := postgres.NewTestConnect() 12 | assert.NoError(t, err) 13 | var queryid string 14 | err = conn.QueryRow( 15 | "SELECT left(md5(userid::text || dbid::text || queryid::text), 10) FROM pg_stat_statements LIMIT 1", 16 | ).Scan(&queryid) 17 | assert.NoError(t, err) 18 | 19 | testcases := []struct { 20 | answer string 21 | want string 22 | }{ 23 | {answer: queryid, want: ""}, 24 | {answer: "", want: "Report: do nothing"}, 25 | {answer: "invalid", want: "Report: no statistics for such queryid"}, 26 | } 27 | 28 | for _, tc := range testcases { 29 | _, got := getQueryReport(tc.answer, 130000, "public", conn) 30 | assert.Equal(t, tc.want, got) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /top/reset.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jroimartin/gocui" 6 | "github.com/lesovsky/pgcenter/internal/postgres" 7 | "github.com/lesovsky/pgcenter/internal/query" 8 | ) 9 | 10 | // resetStat resets Postgres stats counters. 11 | // Reset statistics that belongs to current database and pg_stat_statements stats. 12 | // Don't reset shared stats, such as bgwriter or archiver. 13 | func resetStat(db *postgres.DB, pgssSchema string) func(g *gocui.Gui, _ *gocui.View) error { 14 | return func(g *gocui.Gui, _ *gocui.View) error { 15 | msg := "Reset statistics." 16 | 17 | _, err := db.Exec(query.ExecResetStats) 18 | if err != nil { 19 | msg = fmt.Sprintf("Reset statistics failed: %s", err) 20 | } 21 | 22 | if pgssSchema != "" { 23 | opts := query.Options{PGSSSchema: pgssSchema} 24 | 25 | q, err := query.Format(query.ExecResetPgStatStatements, opts) 26 | if err != nil { 27 | msg = fmt.Sprintf("Reset statistics failed: %s", err) 28 | } 29 | 30 | _, err = db.Exec(q) 31 | if err != nil { 32 | msg = fmt.Sprintf("Reset pg_stat_statements statistics failed: %s", err) 33 | } 34 | } 35 | 36 | printCmdline(g, msg) 37 | 38 | return nil 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /top/reset_test.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_resetStat(t *testing.T) { 10 | conn, err := postgres.NewTestConnect() 11 | assert.NoError(t, err) 12 | 13 | fn := resetStat(conn, "public") 14 | assert.NoError(t, fn(nil, nil)) 15 | 16 | fn = resetStat(conn, "") 17 | assert.NoError(t, fn(nil, nil)) 18 | 19 | conn.Close() 20 | assert.NoError(t, fn(nil, nil)) 21 | } 22 | -------------------------------------------------------------------------------- /top/stat_test.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jackc/pgconn" 6 | "github.com/jackc/pgx/v4" 7 | "github.com/lesovsky/pgcenter/internal/postgres" 8 | "github.com/stretchr/testify/assert" 9 | "testing" 10 | ) 11 | 12 | func Test_formatInfoString(t *testing.T) { 13 | testcases := []struct { 14 | cfg postgres.Config 15 | want string 16 | }{ 17 | { 18 | cfg: postgres.Config{Config: &pgx.ConnConfig{Config: pgconn.Config{Host: "127.0.0.1", Port: 1234, User: "test", Database: "testdb"}}}, 19 | want: "state [up]: 127.0.0.1:1234 test@testdb (ver: 13.1 on x86_64-~, up 01:23:48, recovery: f)", 20 | }, 21 | { 22 | cfg: postgres.Config{Config: &pgx.ConnConfig{Config: pgconn.Config{Host: "127.0.0.1", Port: 1234, User: "test", Database: ""}}}, 23 | want: "state [up]: 127.0.0.1:1234 test@test (ver: 13.1 on x86_64-~, up 01:23:48, recovery: f)", 24 | }, 25 | } 26 | 27 | for _, tc := range testcases { 28 | assert.Equal(t, tc.want, formatInfoString(tc.cfg, "up", "13.1 on x86_64-pc-linux-gnu Debian", "01:23:48", "f")) 29 | } 30 | } 31 | 32 | func Test_formatError(t *testing.T) { 33 | testcases := []struct { 34 | err error 35 | want string 36 | }{ 37 | {err: nil, want: ""}, 38 | { 39 | err: &pgconn.PgError{Severity: "TEST", Message: "test message", Detail: "test detail", Hint: "test hint"}, 40 | want: "TEST: test message\nDETAIL: test detail\nHINT: test hint", 41 | }, 42 | {err: fmt.Errorf("example error"), want: "ERROR: example error"}, 43 | } 44 | 45 | for _, tc := range testcases { 46 | got := formatError(tc.err) 47 | assert.Equal(t, tc.want, got) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /top/top.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "context" 5 | "github.com/jroimartin/gocui" 6 | "github.com/lesovsky/pgcenter/internal/postgres" 7 | "github.com/lesovsky/pgcenter/internal/query" 8 | "github.com/lesovsky/pgcenter/internal/stat" 9 | ) 10 | 11 | // RunMain is the main entry point for 'pgcenter top' command 12 | func RunMain(dbConfig postgres.Config) error { 13 | // Connect to Postgres. 14 | db, err := postgres.Connect(dbConfig) 15 | if err != nil { 16 | return err 17 | } 18 | defer db.Close() 19 | 20 | // Create application instance. 21 | app := newApp(db, newConfig()) 22 | 23 | // Setup application. 24 | err = app.setup() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | // Run application workers and UI. 30 | return mainLoop(context.Background(), app) 31 | } 32 | 33 | // app defines application and all necessary dependencies. 34 | type app struct { 35 | config *config // runtime configuration. 36 | ui *gocui.Gui // UI instance. 37 | uiExit chan int // used for signaling when to need exiting from UI. 38 | uiError error // hold error occurred during executing UI. 39 | db *postgres.DB // connection to Postgres. 40 | postgresProps stat.PostgresProperties // properties of Postgres to which connected to. 41 | } 42 | 43 | // newApp creates new application instance. 44 | func newApp(db *postgres.DB, config *config) *app { 45 | return &app{ 46 | config: config, 47 | db: db, 48 | } 49 | } 50 | 51 | // setup performs initial application setup based on Postgres settings to which application connected to. 52 | func (app *app) setup() error { 53 | // Fetch Postgres properties. 54 | props, err := stat.GetPostgresProperties(app.db) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | // Create query options needed for formatting necessary queries. 60 | opts := query.NewOptions(props.VersionNum, props.Recovery, props.GucTrackCommitTimestamp, 256, props.ExtPGSSSchema) 61 | 62 | // Create and configure stats views adjusting them depending on running Postgres. 63 | err = app.config.views.Configure(opts) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | // Set default view. 69 | app.config.view = app.config.views["activity"] 70 | 71 | app.config.queryOptions = opts 72 | app.postgresProps = props 73 | app.uiExit = make(chan int) 74 | 75 | return nil 76 | } 77 | 78 | // quit performs graceful application quit. 79 | func (app *app) quit() func(g *gocui.Gui, _ *gocui.View) error { 80 | return func(g *gocui.Gui, _ *gocui.View) error { 81 | close(app.uiExit) 82 | g.Close() 83 | app.db.Close() 84 | return gocui.ErrQuit 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /top/top_test.go: -------------------------------------------------------------------------------- 1 | package top 2 | 3 | import ( 4 | "github.com/lesovsky/pgcenter/internal/postgres" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_newApp(t *testing.T) { 10 | conn, err := postgres.NewTestConnect() 11 | assert.NoError(t, err) 12 | 13 | config := newConfig() 14 | assert.NotNil(t, newApp(conn, config)) 15 | defer conn.Close() 16 | } 17 | 18 | func Test_app_setup(t *testing.T) { 19 | conn, err := postgres.NewTestConnect() 20 | assert.NoError(t, err) 21 | 22 | app := newApp(conn, newConfig()) 23 | assert.NotNil(t, app) 24 | 25 | // before setup, all query texts are empty 26 | for _, v := range app.config.views { 27 | assert.Equal(t, "", v.Query) 28 | } 29 | 30 | assert.NoError(t, app.setup()) 31 | 32 | // after setup, all query texts are created from templates 33 | for _, v := range app.config.views { 34 | assert.NotEqual(t, "", v.Query) 35 | } 36 | 37 | // test with closed Postgres connection. 38 | conn.Close() 39 | assert.Error(t, app.setup()) 40 | } 41 | 42 | // This test hangs when executing on Github Actions due to hangs here: 43 | // github.com/nsf/termbox-go@v0.0.0-20180819125858-b66b20ab708e/api.go:122 44 | //func Test_app_quit(t *testing.T) { 45 | // conn, err := postgres.NewTestConnect() 46 | // assert.NoError(t, err) 47 | // 48 | // app := newApp(conn, newConfig()) 49 | // assert.NotNil(t, app) 50 | // assert.NoError(t, app.setup()) 51 | // 52 | // ui, err := gocui.NewGui(gocui.OutputNormal) 53 | // assert.NoError(t, err) 54 | // 55 | // app.ui = ui 56 | // fn := app.quit() 57 | // 58 | // assert.Equal(t, gocui.ErrQuit, fn(app.ui, nil)) 59 | //} 60 | --------------------------------------------------------------------------------