From 5a5e21375e9fe51525321e0bb85d82ae4c0aa2f1 Mon Sep 17 00:00:00 2001 From: woblerr Date: Sat, 11 Apr 2026 17:35:08 +0300 Subject: [PATCH 01/12] Port gpbackup_exporter to cloudberry-backup. Introduce the Prometheus metrics exporter as a new standalone binary within the repository. - Create gpbackup_exporter.go entry point with CLI flags and collection loop. - Port core exporter logic to the new `exporter/` package. - Adapt type system to use `*history.BackupConfig` directly, aligning with the cloudberry-backup architecture. - Switch receiver methods to standalone gpbckpconfig helpers. - Add Prometheus and Kingpin dependencies to `go.mod`. - Update Makefile to support building, testing, and packaging. - Port unit tests to Ginkgo/Gomega. --- Makefile | 14 +- exporter/exporter_suite_test.go | 32 +++ exporter/gpbckp_backup_metrics.go | 177 +++++++++++++++ exporter/gpbckp_backup_metrics_test.go | 109 +++++++++ exporter/gpbckp_converters.go | 46 ++++ exporter/gpbckp_converters_test.go | 56 +++++ exporter/gpbckp_exporter.go | 188 ++++++++++++++++ exporter/gpbckp_exporter_metrics.go | 52 +++++ exporter/gpbckp_exporter_test.go | 231 +++++++++++++++++++ exporter/gpbckp_last_backup_metrics.go | 59 +++++ exporter/gpbckp_last_backup_metrics_test.go | 96 ++++++++ exporter/gpbckp_parser.go | 170 ++++++++++++++ exporter/gpbckp_parser_test.go | 235 ++++++++++++++++++++ go.mod | 40 +++- go.sum | 103 +++++++-- gpbackup_exporter.go | 172 ++++++++++++++ 16 files changed, 1748 insertions(+), 32 deletions(-) create mode 100644 exporter/exporter_suite_test.go create mode 100644 exporter/gpbckp_backup_metrics.go create mode 100644 exporter/gpbckp_backup_metrics_test.go create mode 100644 exporter/gpbckp_converters.go create mode 100644 exporter/gpbckp_converters_test.go create mode 100644 exporter/gpbckp_exporter.go create mode 100644 exporter/gpbckp_exporter_metrics.go create mode 100644 exporter/gpbckp_exporter_test.go create mode 100644 exporter/gpbckp_last_backup_metrics.go create mode 100644 exporter/gpbckp_last_backup_metrics_test.go create mode 100644 exporter/gpbckp_parser.go create mode 100644 exporter/gpbckp_parser_test.go create mode 100644 gpbackup_exporter.go diff --git a/Makefile b/Makefile index 12b5fd44..3bab4d81 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ RESTORE=gprestore HELPER=gpbackup_helper S3PLUGIN=gpbackup_s3_plugin GPBACKMAN=gpbackman +EXPORTER=gpbackup_exporter BIN_DIR=$(shell echo $${GOPATH:-~/go} | awk -F':' '{ print $$1 "/bin"}') GINKGO_FLAGS := -r --keep-going --randomize-suites --randomize-all --no-color GIT_VERSION := $(shell v=$$(git describe --tags 2>/dev/null); if [ -n "$$v" ]; then echo $$v | perl -pe 's/(.*)-([0-9]*)-(g[0-9a-f]*)/\1+dev.\2.\3/'; else cat VERSION 2>/dev/null || echo "dev"; fi) @@ -18,9 +19,13 @@ RESTORE_VERSION_STR=github.com/apache/cloudberry-backup/restore.version=$(GIT_VE HELPER_VERSION_STR=github.com/apache/cloudberry-backup/helper.version=$(GIT_VERSION) S3PLUGIN_VERSION_STR=github.com/apache/cloudberry-backup/plugins/s3plugin.version=$(GIT_VERSION) GPBACKMAN_VERSION_STR=github.com/apache/cloudberry-backup/gpbackman/cmd.version=$(GIT_VERSION) +GIT_REVISION := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") +BUILD_DATE := $(shell date -u '+%Y%m%d-%H:%M:%S') +EXPORTER_VERSION_STR=-X github.com/prometheus/common/version.Version=$(GIT_VERSION) -X github.com/prometheus/common/version.Revision=$(GIT_REVISION) -X github.com/prometheus/common/version.Branch=$(GIT_BRANCH) -X github.com/prometheus/common/version.BuildDate=$(BUILD_DATE) # note that /testutils is not a production directory, but has unit tests to validate testing tools -SUBDIRS_HAS_UNIT=backup/ filepath/ history/ helper/ options/ report/ restore/ toc/ utils/ testutils/ plugins/s3plugin/ gpbackman/cmd/ gpbackman/gpbckpconfig/ gpbackman/textmsg/ +SUBDIRS_HAS_UNIT=backup/ filepath/ history/ helper/ options/ report/ restore/ toc/ utils/ testutils/ plugins/s3plugin/ gpbackman/cmd/ gpbackman/gpbckpconfig/ gpbackman/textmsg/ exporter/ SUBDIRS_ALL=$(SUBDIRS_HAS_UNIT) integration/ end_to_end/ GOLANG_LINTER=$(GOPATH)/bin/golangci-lint GINKGO=$(GOPATH)/bin/ginkgo @@ -90,6 +95,7 @@ build : $(GOSQLITE) CGO_ENABLED=1 $(GO_BUILD) -tags '$(HELPER)' -o $(BIN_DIR)/$(HELPER) --ldflags '-X $(HELPER_VERSION_STR)' CGO_ENABLED=1 $(GO_BUILD) -tags '$(S3PLUGIN)' -o $(BIN_DIR)/$(S3PLUGIN) --ldflags '-X $(S3PLUGIN_VERSION_STR)' CGO_ENABLED=1 $(GO_BUILD) -tags '$(GPBACKMAN)' -o $(BIN_DIR)/$(GPBACKMAN) --ldflags '-X $(GPBACKMAN_VERSION_STR)' + CGO_ENABLED=1 $(GO_BUILD) -tags '$(EXPORTER)' -o $(BIN_DIR)/$(EXPORTER) -ldflags "$(EXPORTER_VERSION_STR)" debug : CGO_ENABLED=1 $(GO_BUILD) -tags '$(BACKUP)' -o $(BIN_DIR)/$(BACKUP) -ldflags "-X $(BACKUP_VERSION_STR)" $(DEBUG) @@ -97,6 +103,7 @@ debug : CGO_ENABLED=1 $(GO_BUILD) -tags '$(HELPER)' -o $(BIN_DIR)/$(HELPER) -ldflags "-X $(HELPER_VERSION_STR)" $(DEBUG) CGO_ENABLED=1 $(GO_BUILD) -tags '$(S3PLUGIN)' -o $(BIN_DIR)/$(S3PLUGIN) -ldflags "-X $(S3PLUGIN_VERSION_STR)" $(DEBUG) CGO_ENABLED=1 $(GO_BUILD) -tags '$(GPBACKMAN)' -o $(BIN_DIR)/$(GPBACKMAN) -ldflags "-X $(GPBACKMAN_VERSION_STR)" $(DEBUG) + CGO_ENABLED=1 $(GO_BUILD) -tags '$(EXPORTER)' -o $(BIN_DIR)/$(EXPORTER) -ldflags "$(EXPORTER_VERSION_STR)" $(DEBUG) build_linux : env GOOS=linux GOARCH=amd64 $(GO_BUILD) -tags '$(BACKUP)' -o $(BACKUP) -ldflags "-X $(BACKUP_VERSION_STR)" @@ -104,6 +111,7 @@ build_linux : env GOOS=linux GOARCH=amd64 $(GO_BUILD) -tags '$(HELPER)' -o $(HELPER) -ldflags "-X $(HELPER_VERSION_STR)" env GOOS=linux GOARCH=amd64 $(GO_BUILD) -tags '$(S3PLUGIN)' -o $(S3PLUGIN) -ldflags "-X $(S3PLUGIN_VERSION_STR)" env GOOS=linux GOARCH=amd64 $(GO_BUILD) -tags '$(GPBACKMAN)' -o $(GPBACKMAN) -ldflags "-X $(GPBACKMAN_VERSION_STR)" + env GOOS=linux GOARCH=amd64 $(GO_BUILD) -tags '$(EXPORTER)' -o $(EXPORTER) -ldflags "$(EXPORTER_VERSION_STR)" install : cp $(BIN_DIR)/$(BACKUP) $(BIN_DIR)/$(RESTORE) $(BIN_DIR)/$(GPBACKMAN) $(GPHOME)/bin @@ -124,7 +132,7 @@ install : clean : # Build artifacts - rm -f $(BIN_DIR)/$(BACKUP) $(BACKUP) $(BIN_DIR)/$(RESTORE) $(RESTORE) $(BIN_DIR)/$(HELPER) $(HELPER) $(BIN_DIR)/$(S3PLUGIN) $(S3PLUGIN) $(BIN_DIR)/$(GPBACKMAN) $(GPBACKMAN) + rm -f $(BIN_DIR)/$(BACKUP) $(BACKUP) $(BIN_DIR)/$(RESTORE) $(RESTORE) $(BIN_DIR)/$(HELPER) $(HELPER) $(BIN_DIR)/$(S3PLUGIN) $(S3PLUGIN) $(BIN_DIR)/$(GPBACKMAN) $(GPBACKMAN) $(BIN_DIR)/$(EXPORTER) $(EXPORTER) # Test artifacts rm -rf /tmp/go-build* /tmp/gexec_artifacts* /tmp/ginkgo* docker stop s3-minio # stop minio before removing its data directories @@ -184,6 +192,7 @@ package: @GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO) go build -tags '$(HELPER)' -o $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/bin/$(HELPER) --ldflags '-X $(HELPER_VERSION_STR)' @GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO) go build -tags '$(S3PLUGIN)' -o $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/bin/$(S3PLUGIN) --ldflags '-X $(S3PLUGIN_VERSION_STR)' @GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO) go build -tags '$(GPBACKMAN)' -o $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/bin/$(GPBACKMAN) --ldflags '-X $(GPBACKMAN_VERSION_STR)' + @GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO) go build -tags '$(EXPORTER)' -o $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/bin/$(EXPORTER) -ldflags "$(EXPORTER_VERSION_STR)" @echo "Copying Apache compliance files..." @cp LICENSE $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/ @cp NOTICE $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/ @@ -214,6 +223,7 @@ package: @echo 'sudo chmod 755 "$${INSTALL_DIR}/bin/$(HELPER)"' >> $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/install.sh @echo 'sudo chmod 755 "$${INSTALL_DIR}/bin/$(S3PLUGIN)"' >> $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/install.sh @echo 'sudo chmod 755 "$${INSTALL_DIR}/bin/$(GPBACKMAN)"' >> $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/install.sh + @echo 'sudo chmod 755 "$${INSTALL_DIR}/bin/$(EXPORTER)"' >> $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/install.sh @echo '' >> $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/install.sh @echo 'echo "Installation complete!"' >> $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/install.sh @echo 'echo "$(PACKAGE_NAME) binaries installed to $${INSTALL_DIR}/bin/"' >> $(BUILD_DIR)/$(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(GOOS)-$(GOARCH)/install.sh diff --git a/exporter/exporter_suite_test.go b/exporter/exporter_suite_test.go new file mode 100644 index 00000000..1b90b423 --- /dev/null +++ b/exporter/exporter_suite_test.go @@ -0,0 +1,32 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestExporter(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Exporter Suite") +} diff --git a/exporter/gpbckp_backup_metrics.go b/exporter/gpbckp_backup_metrics.go new file mode 100644 index 00000000..f0439cd0 --- /dev/null +++ b/exporter/gpbckp_backup_metrics.go @@ -0,0 +1,177 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "log/slog" + "strconv" + + "github.com/apache/cloudberry-backup/gpbackman/gpbckpconfig" + "github.com/apache/cloudberry-backup/history" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + gpbckpBackupStatusMetric = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "gpbackup_backup_status", + Help: "Backup status.", + }, + []string{ + "backup_type", + "database_name", + "object_filtering", + "plugin", + "timestamp"}) + gpbckpBackupDataDeletedStatusMetric = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "gpbackup_backup_deletion_status", + Help: "Backup deletion status.", + }, + []string{ + "backup_type", + "database_name", + "date_deleted", + "object_filtering", + "plugin", + "timestamp"}) + gpbckpBackupInfoMetric = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "gpbackup_backup_info", + Help: "Backup info.", + }, + []string{ + "backup_dir", + "backup_ver", + "backup_type", + "compression_type", + "database_name", + "database_ver", + "object_filtering", + "plugin", + "plugin_ver", + "timestamp", + "with_statistic"}) + gpbckpBackupDurationMetric = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "gpbackup_backup_duration_seconds", + Help: "Backup duration.", + }, + []string{ + "backup_type", + "database_name", + "end_time", + "object_filtering", + "plugin", + "timestamp"}) +) + +// Set backup metrics: +// - gpbackup_backup_status +// - gpbackup_backup_deletion_status +// - gpbackup_backup_info +// - gpbackup_backup_duration_seconds +func getBackupMetrics(backupData *history.BackupConfig, setUpMetricValueFun setUpMetricValueFunType, logger *slog.Logger) { + var ( + bckpDuration float64 + err error + ) + bckpType, err := gpbckpconfig.GetBackupType(backupData) + if err != nil { + logger.Error("Parse backup type value failed", "err", err) + } + backpObjectFiltering, err := gpbckpconfig.GetObjectFilteringInfo(backupData) + if err != nil { + logger.Error("Parse object filtering value failed", "err", err) + } + bckpDuration, err = gpbckpconfig.GetBackupDuration(backupData) + if err != nil { + logger.Error( + "Failed to parse dates to calculate duration", + "err", err, + ) + } + bckpDateDeleted, bckpDeletedStatus := getDeletedStatusCode(backupData.DateDeleted) + // Backup status. + setUpMetric( + gpbckpBackupStatusMetric, + "gpbackup_backup_status", + convertStatusFloat64(backupData.Status), + setUpMetricValueFun, + logger, + bckpType, + backupData.DatabaseName, + convertEmptyLabel(backpObjectFiltering), + convertEmptyLabel(backupData.Plugin), + backupData.Timestamp, + ) + // Backup deletion status. + setUpMetric( + gpbckpBackupDataDeletedStatusMetric, + "gpbackup_backup_deletion_status", + bckpDeletedStatus, + setUpMetricValueFun, + logger, + bckpType, + backupData.DatabaseName, + bckpDateDeleted, + convertEmptyLabel(backpObjectFiltering), + convertEmptyLabel(backupData.Plugin), + backupData.Timestamp, + ) + // Backup info. + setUpMetric( + gpbckpBackupInfoMetric, + "gpbackup_backup_info", + 1, + setUpMetricValueFun, + logger, + convertEmptyLabel(backupData.BackupDir), + backupData.BackupVersion, + bckpType, + convertEmptyLabel(backupData.CompressionType), + backupData.DatabaseName, + backupData.DatabaseVersion, + convertEmptyLabel(backpObjectFiltering), + convertEmptyLabel(backupData.Plugin), + convertEmptyLabel(backupData.PluginVersion), + backupData.Timestamp, + strconv.FormatBool(backupData.WithStatistics), + ) + // Backup duration. + setUpMetric( + gpbckpBackupDurationMetric, + "gpbackup_backup_duration_seconds", + bckpDuration, + setUpMetricValueFun, + logger, + bckpType, + backupData.DatabaseName, + // End time may be not set, if backup in progress. + convertEmptyLabel(backupData.EndTime), + convertEmptyLabel(backpObjectFiltering), + convertEmptyLabel(backupData.Plugin), + backupData.Timestamp, + ) +} + +func resetBackupMetrics() { + gpbckpBackupStatusMetric.Reset() + gpbckpBackupDataDeletedStatusMetric.Reset() + gpbckpBackupInfoMetric.Reset() + gpbckpBackupDurationMetric.Reset() +} diff --git a/exporter/gpbckp_backup_metrics_test.go b/exporter/gpbckp_backup_metrics_test.go new file mode 100644 index 00000000..17ca87d1 --- /dev/null +++ b/exporter/gpbckp_backup_metrics_test.go @@ -0,0 +1,109 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "bytes" + "fmt" + "log/slog" + "strings" + + "github.com/apache/cloudberry-backup/history" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/expfmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("BackupMetrics", func() { + // All metrics exist and all labels are corrected. + // gpbackup version >= 1.23.0 + Describe("getBackupMetrics", func() { + It("sets all backup metrics correctly", func() { + resetBackupMetrics() + getBackupMetrics(templateBackupConfig(), setUpMetricValue, getLogger()) + reg := prometheus.NewRegistry() + reg.MustRegister( + gpbckpBackupStatusMetric, + gpbckpBackupDataDeletedStatusMetric, + gpbckpBackupInfoMetric, + gpbckpBackupDurationMetric, + ) + metricFamily, err := reg.Gather() + if err != nil { + fmt.Println(err) + } + out := &bytes.Buffer{} + for _, mf := range metricFamily { + if _, err := expfmt.MetricFamilyToText(out, mf); err != nil { + panic(err) + } + } + templateMetrics := `# HELP gpbackup_backup_deletion_status Backup deletion status. +# TYPE gpbackup_backup_deletion_status gauge +gpbackup_backup_deletion_status{backup_type="full",database_name="test",date_deleted="none",object_filtering="none",plugin="none",timestamp="20230118152654"} 0 +# HELP gpbackup_backup_duration_seconds Backup duration. +# TYPE gpbackup_backup_duration_seconds gauge +gpbackup_backup_duration_seconds{backup_type="full",database_name="test",end_time="20230118152656",object_filtering="none",plugin="none",timestamp="20230118152654"} 2 +# HELP gpbackup_backup_info Backup info. +# TYPE gpbackup_backup_info gauge +gpbackup_backup_info{backup_dir="/data/backups",backup_type="full",backup_ver="1.30.5",compression_type="gzip",database_name="test",database_ver="6.23.0",object_filtering="none",plugin="none",plugin_ver="none",timestamp="20230118152654",with_statistic="false"} 1 +# HELP gpbackup_backup_status Backup status. +# TYPE gpbackup_backup_status gauge +gpbackup_backup_status{backup_type="full",database_name="test",object_filtering="none",plugin="none",timestamp="20230118152654"} 0 +` + Expect(out.String()).To(Equal(templateMetrics)) + }) + }) + + Describe("getBackupMetrics errors and debugs", func() { + DescribeTable("counts errors and debugs correctly", + func(backupData *history.BackupConfig, errorsCount, debugsCount int) { + resetBackupMetrics() + out := &bytes.Buffer{} + lc := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{Level: slog.LevelDebug})) + getBackupMetrics(backupData, fakeSetUpMetricValue, lc) + errorsOutputCount := strings.Count(out.String(), "level=ERROR") + debugsOutputCount := strings.Count(out.String(), "level=DEBUG") + Expect(errorsOutputCount).To(Equal(errorsCount)) + Expect(debugsOutputCount).To(Equal(debugsCount)) + }, + Entry("GetBackupMetricsErrorGetDurationGood", + templateBackupConfig(), + 4, 4, + ), + Entry("GetBackupMetricsErrorGetDurationError", + &history.BackupConfig{}, + 5, 4, + ), + Entry("GetBackupMetricsErrorGetBackupTypeAndObjectFilteringError", + // Fake example for testing. + &history.BackupConfig{ + DataOnly: true, + Incremental: true, + IncludeSchemaFiltered: true, + ExcludeSchemaFiltered: true, + }, + 7, 4, + ), + ) + }) +}) diff --git a/exporter/gpbckp_converters.go b/exporter/gpbckp_converters.go new file mode 100644 index 00000000..a4836c86 --- /dev/null +++ b/exporter/gpbckp_converters.go @@ -0,0 +1,46 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import "github.com/apache/cloudberry-backup/history" + +// Convert bool to float64. +func convertBoolToFloat64(value bool) float64 { + if value { + return 1 + } + return 0 +} + +// Convert backup status to float64. +func convertStatusFloat64(valueStatus string) float64 { + if valueStatus == history.BackupStatusFailed { + return 1 + } + return 0 +} + +// Convert empty string to empty label ("none" value). +func convertEmptyLabel(str string) string { + if str == "" { + return emptyLabel + } + return str +} diff --git a/exporter/gpbckp_converters_test.go b/exporter/gpbckp_converters_test.go new file mode 100644 index 00000000..75db69a0 --- /dev/null +++ b/exporter/gpbckp_converters_test.go @@ -0,0 +1,56 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "github.com/apache/cloudberry-backup/history" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Converters", func() { + Describe("convertBoolToFloat64", func() { + It("returns 1 for true", func() { + Expect(convertBoolToFloat64(true)).To(Equal(float64(1))) + }) + It("returns 0 for false", func() { + Expect(convertBoolToFloat64(false)).To(Equal(float64(0))) + }) + }) + + Describe("convertEmptyLabel", func() { + It("returns 'none' for empty string", func() { + Expect(convertEmptyLabel("")).To(Equal("none")) + }) + It("returns original string for non-empty string", func() { + Expect(convertEmptyLabel("text")).To(Equal("text")) + }) + }) + + Describe("convertStatusFloat64", func() { + It("returns 1 for Failure status", func() { + Expect(convertStatusFloat64(history.BackupStatusFailed)).To(Equal(float64(1))) + }) + It("returns 0 for non-Failure status", func() { + Expect(convertStatusFloat64("text")).To(Equal(float64(0))) + }) + }) +}) diff --git a/exporter/gpbckp_exporter.go b/exporter/gpbckp_exporter.go new file mode 100644 index 00000000..d384f8a9 --- /dev/null +++ b/exporter/gpbckp_exporter.go @@ -0,0 +1,188 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "log/slog" + "net/http" + "os" + "time" + + "github.com/apache/cloudberry-backup/gpbackman/gpbckpconfig" + "github.com/apache/cloudberry-backup/history" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/exporter-toolkit/web" +) + +var ( + webFlagsConfig web.FlagConfig + webEndpoint string +) + +// SetPromPortAndPath sets HTTP endpoint parameters. +func SetPromPortAndPath(flagsConfig web.FlagConfig, endpoint string) { + webFlagsConfig = flagsConfig + webEndpoint = endpoint +} + +// StartPromEndpoint run HTTP endpoint. +func StartPromEndpoint(version string, logger *slog.Logger) { + go func(logger *slog.Logger) { + if webEndpoint == "" { + logger.Error("Metric endpoint is empty", "endpoint", webEndpoint) + } + http.Handle(webEndpoint, promhttp.Handler()) + if webEndpoint != "/" { + landingConfig := web.LandingConfig{ + Name: "cloudberry-backup exporter", + Description: "Prometheus exporter for Apache Cloudberry (Incubating) backup utility", + HeaderColor: "#476b6b", + Version: version, + Profiling: "false", + Links: []web.LandingLinks{ + { + Address: webEndpoint, + Text: "Metrics", + }, + }, + } + landingPage, err := web.NewLandingPage(landingConfig) + if err != nil { + logger.Error("Error creating landing page", "err", err) + os.Exit(1) + } + http.Handle("/", landingPage) + } + server := &http.Server{ + ReadHeaderTimeout: 5 * time.Second, + } + if err := web.ListenAndServe(server, &webFlagsConfig, logger); err != nil { + logger.Error("Run web endpoint failed", "err", err) + os.Exit(1) + } + }(logger) +} + +// GetGPBackupInfo get and parse gpbackup history database. +func GetGPBackupInfo(historyFile, backupType string, collectDeleted, collectFailed bool, dbInclude, dbExclude []string, collectDepth int, logger *slog.Logger) { + // The flag indicates whether it was possible to get data from the gpbackup history. + // By default, it's set to true. + getDataSuccessStatus := true + // To calculate the time elapsed since the last completed backup for specific database. + // For all databases values are calculated relative to one value. + currentTime := time.Now() + currentUnixTime := currentTime.Unix() + // Calculate metrics collection depth. + // For backups with timestamp older than this - metrics doesn't collect. + collectDepthTime := currentTime.AddDate(0, 0, -collectDepth) + // The backup number can be reduced using filters for deleted and failed backups. + backupConfigs, err := parseBackupData(historyFile, collectDeleted, collectFailed, logger) + if err != nil { + logger.Error("Get data failed", "err", err) + getDataSuccessStatus = false + } + // Reset metrics. + resetMetrics() + if len(backupConfigs) != 0 { + // Like lastbackups["testDB"]["full"] = time + lastBackups := make(lastBackupMap) + dbStatus := make(dbStatusMap) + for i := 0; i < len(backupConfigs); i++ { + db := backupConfigs[i].DatabaseName + // If the same database is specified in include and exclude list, + // then metrics for this database will not be collected. + if !dbInList(db, dbExclude) { + if listEmpty(dbInclude) || dbInList(db, dbInclude) { + dbStatus[db] = getDataSuccessStatus + bckpType, err := gpbckpconfig.GetBackupType(backupConfigs[i]) + if err != nil { + logger.Error("Parse backup type value failed", "err", err) + } + // Check backup type and compare with backup type filter. + if backupType == "" || backupType == bckpType { + // History file contains backup timestamp and endtime with timezone information. + // It is necessary to take this into account when calculating time intervals. + // With a high probability, the exporter will work in the same timezone as Greenplum cluster. + // If this is not the case, then there are many questions about the backup process. + bckpStartTime, err := time.ParseInLocation(gpbckpconfig.Layout, backupConfigs[i].Timestamp, time.Local) + if err != nil { + logger.Error("Parse backup timestamp value failed", "err", err) + } + bckpStopTime, err := time.ParseInLocation(gpbckpconfig.Layout, backupConfigs[i].EndTime, time.Local) + if err != nil { + logger.Error("Parse backup end time value failed", "err", err) + } + // Only if set correct value for collectDepth. + if collectDepth > 0 { + // gpbackup_history.db is sorted by timestamp values. + // The data of the most recent backup is always located at the beginning. + // So as soon as we get the first value that is older than collectDepthTime, + // the cycle can be broken. + // If this behavior ever changes, then this code needs to be refactored. + if collectDepthTime.Before(bckpStartTime) { + getBackupMetrics(backupConfigs[i], setUpMetricValue, logger) + } else { + break + } + } else { + getBackupMetrics(backupConfigs[i], setUpMetricValue, logger) + } + if backupConfigs[i].Status == history.BackupStatusSucceed { + // Check specific database key already exist. + if dbLastBackups, ok := lastBackups[db]; ok { + // Check specific backup type key already exist. + if _, ok := dbLastBackups[bckpType]; !ok { + dbLastBackups[bckpType] = bckpStopTime + } + // A small note on the code above. + // Since the history file is already sorted, the first occurrence will be the last backup. + // However, if sorting is suddenly removed in the future, the code should be something like this: + // if curLastTime, ok := dbLastBackups[bckpType]; ok { + // if curLastTime.Before(bckpStopTime) { + // dbLastBackups[bckpType] = bckpStopTime + // } + // } else { + // dbLastBackups[bckpType] = bckpStopTime + // } + } else { + lastBackups[db] = backupMap{bckpType: bckpStopTime} + } + } + } + } + } else if dbInList(db, dbInclude) { + // When db is specified in both include and exclude lists, a warning is displayed in the log + // and data for this db is not collected. + // It is necessary to set zero metric value for this db. + getDataSuccessStatus = false + dbStatus[db] = getDataSuccessStatus + logger.Warn("DB is specified in include and exclude lists", "DB", db) + } + } + if len(lastBackups) != 0 { + getBackupLastMetrics(lastBackups, currentUnixTime, setUpMetricValue, logger) + } else { + logger.Warn("No succeed backups") + } + getExporterStatusMetrics(dbStatus, setUpMetricValue, logger) + } else { + logger.Warn("No backup data returned") + } +} diff --git a/exporter/gpbckp_exporter_metrics.go b/exporter/gpbckp_exporter_metrics.go new file mode 100644 index 00000000..a995b95d --- /dev/null +++ b/exporter/gpbckp_exporter_metrics.go @@ -0,0 +1,52 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "log/slog" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var gpbckpExporterStatusMetric = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "gpbackup_exporter_status", + Help: "gpbackup exporter get data status.", +}, + []string{"database_name"}) + +// Set exporter metrics: +// - gpbackup_exporter_status +func getExporterStatusMetrics(dbStatus dbStatusMap, setUpMetricValueFun setUpMetricValueFunType, logger *slog.Logger) { + for dbName, status := range dbStatus { + setUpMetric( + gpbckpExporterStatusMetric, + "gpbackup_exporter_status", + convertBoolToFloat64(status), + setUpMetricValueFun, + logger, + dbName, + ) + } +} + +func resetExporterMetrics() { + gpbckpExporterStatusMetric.Reset() +} diff --git a/exporter/gpbckp_exporter_test.go b/exporter/gpbckp_exporter_test.go new file mode 100644 index 00000000..0212c05d --- /dev/null +++ b/exporter/gpbckp_exporter_test.go @@ -0,0 +1,231 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "bytes" + "log/slog" + "os" + "strings" + + "github.com/apache/cloudberry-backup/history" + "github.com/prometheus/exporter-toolkit/web" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Create a test history database from backup configs. +func fakeHistoryFileData(backupConfigs []*history.BackupConfig) (string, error) { + tempFile, err := os.CreateTemp("", "gpbackup_history*.db") + if err != nil { + return "", err + } + tempFile.Close() + hDB, err := history.InitializeHistoryDatabase(tempFile.Name()) + if err != nil { + return "", err + } + defer hDB.Close() + for _, config := range backupConfigs { + err = history.StoreBackupHistory(hDB, config) + if err != nil { + return "", err + } + } + return tempFile.Name(), nil +} + +var _ = Describe("Exporter", func() { + Describe("SetPromPortAndPath", func() { + It("sets web flags config and endpoint", func() { + testFlagsConfig := web.FlagConfig{ + WebListenAddresses: &([]string{":9854"}), + WebSystemdSocket: func(i bool) *bool { return &i }(false), + WebConfigFile: func(i string) *string { return &i }(""), + } + testEndpoint := "/metrics" + SetPromPortAndPath(testFlagsConfig, testEndpoint) + Expect(webFlagsConfig.WebListenAddresses).To(BeIdenticalTo(testFlagsConfig.WebListenAddresses)) + Expect(webFlagsConfig.WebSystemdSocket).To(BeIdenticalTo(testFlagsConfig.WebSystemdSocket)) + Expect(webFlagsConfig.WebConfigFile).To(BeIdenticalTo(testFlagsConfig.WebConfigFile)) + Expect(webEndpoint).To(Equal(testEndpoint)) + }) + }) + + Describe("fakeHistoryFileData", func() { + It("creates valid db from backup configs", func() { + configs := []*history.BackupConfig{templateBackupConfig()} + dbFile, err := fakeHistoryFileData(configs) + Expect(err).ToNot(HaveOccurred()) + Expect(dbFile).ToNot(BeEmpty()) + defer os.Remove(dbFile) + }) + It("creates empty db from empty config list", func() { + configs := []*history.BackupConfig{} + dbFile, err := fakeHistoryFileData(configs) + Expect(err).ToNot(HaveOccurred()) + Expect(dbFile).ToNot(BeEmpty()) + defer os.Remove(dbFile) + }) + }) + + Describe("GetGPBackupInfo", func() { + It("returns good data for valid backups", func() { + metadataOnlyConfig := templateBackupConfig() + metadataOnlyConfig.MetadataOnly = true + metadataOnlyConfig.Timestamp = "20230118162454" + metadataOnlyConfig.EndTime = "20230118162456" + backupConfigs := []*history.BackupConfig{ + templateBackupConfig(), + metadataOnlyConfig, + } + dbFile, err := fakeHistoryFileData(backupConfigs) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(dbFile) + resetMetrics() + out := &bytes.Buffer{} + lc := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) + GetGPBackupInfo(dbFile, "", false, false, []string{""}, []string{""}, 0, lc) + logOutput := out.String() + // Metadata-only backup (later timestamp) should appear first. + Expect(logOutput).To(ContainSubstring( + `level=DEBUG msg="Set up metric" metric=gpbackup_backup_status value=0 labels=metadata-only,test,none,none,20230118162454`)) + Expect(logOutput).To(ContainSubstring( + `level=DEBUG msg="Set up metric" metric=gpbackup_backup_deletion_status value=0 labels=metadata-only,test,none,none,none,20230118162454`)) + Expect(logOutput).To(ContainSubstring( + `level=DEBUG msg="Set up metric" metric=gpbackup_backup_info value=1 labels=/data/backups,1.30.5,metadata-only,gzip,test,6.23.0,none,none,none,20230118162454,false`)) + Expect(logOutput).To(ContainSubstring( + `level=DEBUG msg="Set up metric" metric=gpbackup_backup_duration_seconds value=2 labels=metadata-only,test,20230118162456,none,none,20230118162454`)) + // Full backup. + Expect(logOutput).To(ContainSubstring( + `level=DEBUG msg="Set up metric" metric=gpbackup_backup_status value=0 labels=full,test,none,none,20230118152654`)) + Expect(logOutput).To(ContainSubstring( + `level=DEBUG msg="Set up metric" metric=gpbackup_backup_deletion_status value=0 labels=full,test,none,none,none,20230118152654`)) + Expect(logOutput).To(ContainSubstring( + `level=DEBUG msg="Set up metric" metric=gpbackup_backup_info value=1 labels=/data/backups,1.30.5,full,gzip,test,6.23.0,none,none,none,20230118152654,false`)) + Expect(logOutput).To(ContainSubstring( + `level=DEBUG msg="Set up metric" metric=gpbackup_backup_duration_seconds value=2 labels=full,test,20230118152656,none,none,20230118152654`)) + }) + + It("warns when no data is returned", func() { + backupConfigs := []*history.BackupConfig{} + dbFile, err := fakeHistoryFileData(backupConfigs) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(dbFile) + resetMetrics() + out := &bytes.Buffer{} + lc := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) + GetGPBackupInfo(dbFile, "", false, false, []string{""}, []string{""}, 0, lc) + Expect(out.String()).To(ContainSubstring(`No backup data returned`)) + }) + + It("warns when using depth and backup is older than depth interval", func() { + backupConfigs := []*history.BackupConfig{templateBackupConfig()} + dbFile, err := fakeHistoryFileData(backupConfigs) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(dbFile) + resetMetrics() + out := &bytes.Buffer{} + lc := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) + GetGPBackupInfo(dbFile, "", false, false, []string{""}, []string{""}, 14, lc) + Expect(out.String()).To(ContainSubstring(`No succeed backups`)) + }) + + It("warns when db is in both include and exclude lists", func() { + backupConfigs := []*history.BackupConfig{templateBackupConfig()} + dbFile, err := fakeHistoryFileData(backupConfigs) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(dbFile) + resetMetrics() + out := &bytes.Buffer{} + lc := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) + GetGPBackupInfo(dbFile, "", false, false, []string{"test"}, []string{"test"}, 0, lc) + Expect(out.String()).To(ContainSubstring(`DB is specified in include and exclude lists`)) + Expect(out.String()).To(ContainSubstring(`DB=test`)) + }) + + It("logs errors for invalid backup values", func() { + invalidConfig := &history.BackupConfig{ + DatabaseName: "test", + DataOnly: true, + Incremental: true, + IncludeSchemaFiltered: true, + ExcludeSchemaFiltered: true, + Timestamp: "test", + EndTime: "test", + Status: history.BackupStatusSucceed, + } + backupConfigs := []*history.BackupConfig{invalidConfig} + dbFile, err := fakeHistoryFileData(backupConfigs) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(dbFile) + resetMetrics() + out := &bytes.Buffer{} + lc := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) + GetGPBackupInfo(dbFile, "", false, false, []string{""}, []string{""}, 0, lc) + logOutput := out.String() + Expect(logOutput).To(ContainSubstring(`Parse backup timestamp value failed`)) + // Verify errors were logged (parsing errors + metric setup errors). + errorsCount := strings.Count(logOutput, "level=ERROR") + Expect(errorsCount).To(BeNumerically(">", 0)) + }) + }) +}) diff --git a/exporter/gpbckp_last_backup_metrics.go b/exporter/gpbckp_last_backup_metrics.go new file mode 100644 index 00000000..a96e9262 --- /dev/null +++ b/exporter/gpbckp_last_backup_metrics.go @@ -0,0 +1,59 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "log/slog" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var gpbckpBackupSinceLastCompletionSecondsMetric = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "gpbackup_backup_since_last_completion_seconds", + Help: "Seconds since the last completed backup.", +}, + []string{ + "backup_type", + "database_name"}) + +// Set backup metrics: +// - gpbackup_backup_since_last_completion_seconds +func getBackupLastMetrics(lastBackups lastBackupMap, currentUnixTime int64, setUpMetricValueFun setUpMetricValueFunType, logger *slog.Logger) { + for db, bckps := range lastBackups { + for bckpType, endTime := range bckps { + // Seconds since the last completed backups. + setUpMetric( + gpbckpBackupSinceLastCompletionSecondsMetric, + "gpbackup_backup_since_last_completion_seconds", + time.Unix(currentUnixTime, 0).Sub(endTime).Seconds(), + setUpMetricValueFun, + logger, + bckpType, + db, + ) + } + } +} + +func resetLastBackupMetrics() { + gpbckpBackupSinceLastCompletionSecondsMetric.Reset() +} diff --git a/exporter/gpbckp_last_backup_metrics_test.go b/exporter/gpbckp_last_backup_metrics_test.go new file mode 100644 index 00000000..af2c1557 --- /dev/null +++ b/exporter/gpbckp_last_backup_metrics_test.go @@ -0,0 +1,96 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "bytes" + "fmt" + "log/slog" + "strings" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/expfmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("LastBackupMetrics", func() { + Describe("getBackupLastMetrics", func() { + It("sets last backup metrics correctly", func() { + resetLastBackupMetrics() + getBackupLastMetrics( + lastBackupMap{ + "test": backupMap{ + "full": returnTimeTime("20230118150000"), + "incremental": returnTimeTime("20230118160000"), + "metadata-only": returnTimeTime("20230118170000"), + "data-only": returnTimeTime("20230118180000"), + }, + }, + templateUnixTime(), + setUpMetricValue, + getLogger(), + ) + reg := prometheus.NewRegistry() + reg.MustRegister(gpbckpBackupSinceLastCompletionSecondsMetric) + metricFamily, err := reg.Gather() + if err != nil { + fmt.Println(err) + } + out := &bytes.Buffer{} + for _, mf := range metricFamily { + if _, err := expfmt.MetricFamilyToText(out, mf); err != nil { + panic(err) + } + } + templateMetrics := `# HELP gpbackup_backup_since_last_completion_seconds Seconds since the last completed backup. +# TYPE gpbackup_backup_since_last_completion_seconds gauge +gpbackup_backup_since_last_completion_seconds{backup_type="data-only",database_name="test"} 7200 +gpbackup_backup_since_last_completion_seconds{backup_type="full",database_name="test"} 18000 +gpbackup_backup_since_last_completion_seconds{backup_type="incremental",database_name="test"} 14400 +gpbackup_backup_since_last_completion_seconds{backup_type="metadata-only",database_name="test"} 10800 +` + Expect(out.String()).To(Equal(templateMetrics)) + }) + }) + + Describe("getBackupLastMetrics errors and debugs", func() { + It("counts errors and debugs correctly", func() { + resetLastBackupMetrics() + out := &bytes.Buffer{} + lc := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{Level: slog.LevelDebug})) + getBackupLastMetrics( + lastBackupMap{ + "test": backupMap{ + "full": returnTimeTime("20230118150000"), + }, + }, + templateUnixTime(), + fakeSetUpMetricValue, + lc, + ) + errorsOutputCount := strings.Count(out.String(), "level=ERROR") + debugsOutputCount := strings.Count(out.String(), "level=DEBUG") + Expect(errorsOutputCount).To(Equal(1)) + Expect(debugsOutputCount).To(Equal(1)) + }) + }) +}) diff --git a/exporter/gpbckp_parser.go b/exporter/gpbckp_parser.go new file mode 100644 index 00000000..aaa7e268 --- /dev/null +++ b/exporter/gpbckp_parser.go @@ -0,0 +1,170 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "errors" + "log/slog" + "path/filepath" + "strings" + "time" + + "github.com/apache/cloudberry-backup/gpbackman/gpbckpconfig" + "github.com/apache/cloudberry-backup/history" + "github.com/prometheus/client_golang/prometheus" +) + +const emptyLabel = "none" + +type setUpMetricValueFunType func(metric *prometheus.GaugeVec, value float64, labels ...string) error + +type backupMap map[string]time.Time +type lastBackupMap map[string]backupMap +type dbStatusMap map[string]bool + +func setUpMetricValue(metric *prometheus.GaugeVec, value float64, labels ...string) error { + metricVec, err := metric.GetMetricWithLabelValues(labels...) + if err != nil { + return err + } + // The situation should be handled by the prometheus libraries. + // But, anything is possible. + if metricVec == nil { + err := errors.New("metric is nil") + return err + } + metricVec.Set(value) + return nil +} + +// Get status code about backup deletion status. +// Based on available statuses from gpbackman utility documentation, +// but not limited to that. +// - 0 - backup still exists; +// - 1 - backup was successfully deleted; +// - 2 - the deletion is in progress; +// - 3 - last delete attempt failed to delete backup from plugin storage; +// - 4 - last delete attempt failed to delete backup from local storage; +func getDeletedStatusCode(valueDateDeleted string) (string, float64) { + var ( + dateDeleted string + deletedStatus float64 + ) + switch valueDateDeleted { + case "": + dateDeleted = emptyLabel + deletedStatus = 0 + case gpbckpconfig.DateDeletedInProgress: + dateDeleted = emptyLabel + deletedStatus = 2 + case gpbckpconfig.DateDeletedPluginFailed: + dateDeleted = emptyLabel + deletedStatus = 3 + case gpbckpconfig.DateDeletedLocalFailed: + dateDeleted = emptyLabel + deletedStatus = 4 + default: + dateDeleted = valueDateDeleted + deletedStatus = 1 + } + return dateDeleted, deletedStatus +} + +// Reset all metrics. +func resetMetrics() { + resetBackupMetrics() + resetLastBackupMetrics() + resetExporterMetrics() +} + +func setUpMetric(metric *prometheus.GaugeVec, metricName string, value float64, setUpMetricValueFun setUpMetricValueFunType, logger *slog.Logger, labels ...string) { + logger.Debug( + "Set up metric", + "metric", metricName, + "value", value, + "labels", strings.Join(labels, ","), + ) + err := setUpMetricValueFun(metric, value, labels...) + if err != nil { + logger.Error( + "Metric set up failed", + "metric", metricName, + "err", err, + ) + } +} + +func dbInList(db string, list []string) bool { + if listEmpty(list) { + return false + } + for _, val := range list { + if val == db { + return true + } + } + return false +} + +// Check list not empty. +func listEmpty(list []string) bool { + return strings.Join(list, "") == "" +} + +// Get and parse data from history database: +// - file with extension .db (sqlite). +// +// Returns parsed data or error. +func parseBackupData(historyFile string, collectDeleted, collectFailed bool, logger *slog.Logger) ([]*history.BackupConfig, error) { + if filepath.Ext(historyFile) != ".db" { + return nil, errors.New("file has an extension other than db (sqlite)") + } + return getDataFromHistoryDB(historyFile, collectDeleted, collectFailed, logger) +} + +func getDataFromHistoryDB(historyFile string, collectDeleted, collectFailed bool, logger *slog.Logger) ([]*history.BackupConfig, error) { + hDB, err := gpbckpconfig.OpenHistoryDB(historyFile) + if err != nil { + logger.Error("Open gpbackup history db failed", "err", err) + return nil, err + } + defer func() { + errClose := hDB.Close() + if errClose != nil { + logger.Error("Close gpbackup history db failed", "err", errClose) + } + }() + backupList, err := gpbckpconfig.GetBackupNamesDB(collectDeleted, collectFailed, hDB) + if err != nil { + logger.Error("Get backups from history db failed", "err", err) + return nil, err + } + // Get data for selected backups. + var backupConfigs []*history.BackupConfig + for _, backupName := range backupList { + backupData, err := gpbckpconfig.GetBackupDataDB(backupName, hDB) + if err != nil { + logger.Error("Get backup data from history db failed", "err", err) + return nil, err + } + backupConfigs = append(backupConfigs, backupData) + } + return backupConfigs, nil +} diff --git a/exporter/gpbckp_parser_test.go b/exporter/gpbckp_parser_test.go new file mode 100644 index 00000000..8e44a1bd --- /dev/null +++ b/exporter/gpbckp_parser_test.go @@ -0,0 +1,235 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package exporter + +import ( + "bytes" + "errors" + "log/slog" + "os" + "time" + + "github.com/apache/cloudberry-backup/gpbackman/gpbckpconfig" + "github.com/apache/cloudberry-backup/history" + "github.com/prometheus/client_golang/prometheus" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func getLogger() *slog.Logger { + return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) +} + +func fakeSetUpMetricValue(_ *prometheus.GaugeVec, _ float64, _ ...string) error { + return errors.New("custom error for test") +} + +// Create a SQLite database file with missing tables. +func createCorruptedDBFile() string { + tempFile, err := os.CreateTemp("", "test_corrupted_*.db") + Expect(err).ToNot(HaveOccurred()) + defer tempFile.Close() + return tempFile.Name() +} + +// Create a database with invalid backup name. +func createDBWithInvalidBackupName() string { + tempFile, err := os.CreateTemp("", "test_invalid_backup_*.db") + Expect(err).ToNot(HaveOccurred()) + defer tempFile.Close() + db, err := gpbckpconfig.OpenHistoryDB(tempFile.Name()) + Expect(err).ToNot(HaveOccurred()) + defer db.Close() + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS backups ( + timestamp TEXT PRIMARY KEY, + date_deleted TEXT, + database_name TEXT, + status TEXT + )`) + Expect(err).ToNot(HaveOccurred()) + // Insert a backup with an invalid timestamp. + _, err = db.Exec(`INSERT INTO backups (timestamp, date_deleted, database_name, status) VALUES + ('invalid_backup_name', '', 'testdb', 'Success')`) + Expect(err).ToNot(HaveOccurred()) + return tempFile.Name() +} + +func templateBackupConfig() *history.BackupConfig { + return &history.BackupConfig{ + BackupDir: "/data/backups", + BackupVersion: "1.30.5", + Compressed: true, + CompressionType: "gzip", + DatabaseName: "test", + DatabaseVersion: "6.23.0", + DataOnly: false, + DateDeleted: "", + ExcludeRelations: []string{}, + ExcludeSchemaFiltered: false, + ExcludeSchemas: []string{}, + ExcludeTableFiltered: false, + IncludeRelations: []string{}, + IncludeSchemaFiltered: false, + IncludeSchemas: []string{}, + IncludeTableFiltered: false, + Incremental: false, + LeafPartitionData: false, + MetadataOnly: false, + Plugin: "", + PluginVersion: "", + RestorePlan: []history.RestorePlanEntry{}, + SingleDataFile: false, + Timestamp: "20230118152654", + EndTime: "20230118152656", + WithoutGlobals: false, + WithStatistics: false, + Status: history.BackupStatusSucceed, + } +} + +func templateUnixTime() int64 { + // Thu Jan 18 2023 20:00:00 UTC + var curUnixTime int64 = 1674072000 + return curUnixTime +} + +func returnTimeTime(sTime string) time.Time { + rTime, err := time.Parse(gpbckpconfig.Layout, sTime) + if err != nil { + panic(err) + } + return rTime +} + +var _ = Describe("Parser", func() { + Describe("getDeletedStatusCode", func() { + It("returns 0 for existing backup (empty date)", func() { + dateDeleted, status := getDeletedStatusCode("") + Expect(dateDeleted).To(Equal("none")) + Expect(status).To(Equal(float64(0))) + }) + It("returns 2 for In Progress", func() { + dateDeleted, status := getDeletedStatusCode(gpbckpconfig.DateDeletedInProgress) + Expect(dateDeleted).To(Equal("none")) + Expect(status).To(Equal(float64(2))) + }) + It("returns 3 for Plugin Backup Delete Failed", func() { + dateDeleted, status := getDeletedStatusCode(gpbckpconfig.DateDeletedPluginFailed) + Expect(dateDeleted).To(Equal("none")) + Expect(status).To(Equal(float64(3))) + }) + It("returns 4 for Local Delete Failed", func() { + dateDeleted, status := getDeletedStatusCode(gpbckpconfig.DateDeletedLocalFailed) + Expect(dateDeleted).To(Equal("none")) + Expect(status).To(Equal(float64(4))) + }) + It("returns 1 for valid deletion date", func() { + dateDeleted, status := getDeletedStatusCode("20230118150331") + Expect(dateDeleted).To(Equal("20230118150331")) + Expect(status).To(Equal(float64(1))) + }) + }) + + Describe("setUpMetricValue", func() { + It("returns error when labels don't match", func() { + err := setUpMetricValue(gpbckpExporterStatusMetric, 0, "demo", "bad") + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("dbInList", func() { + It("returns true when db is in list", func() { + Expect(dbInList("test", []string{"test"})).To(BeTrue()) + }) + It("returns false when db is not in list", func() { + Expect(dbInList("test", []string{"demo"})).To(BeFalse()) + }) + It("returns false for empty list", func() { + Expect(dbInList("test", []string{""})).To(BeFalse()) + }) + }) + + Describe("listEmpty", func() { + It("returns true for empty list", func() { + Expect(listEmpty([]string{})).To(BeTrue()) + }) + It("returns false for non-empty list", func() { + Expect(listEmpty([]string{"a", "b", "c"})).To(BeFalse()) + }) + }) + + Describe("parseBackupData", func() { + It("returns error for yaml file", func() { + tempFile, err := os.CreateTemp("", "test*.yaml") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tempFile.Name()) + got, err := parseBackupData(tempFile.Name(), false, false, getLogger()) + Expect(err).To(HaveOccurred()) + Expect(got).To(BeNil()) + }) + It("returns error for empty db file", func() { + tempFile, err := os.CreateTemp("", "test*.db") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tempFile.Name()) + got, err := parseBackupData(tempFile.Name(), false, false, getLogger()) + Expect(err).To(HaveOccurred()) + Expect(got).To(BeNil()) + }) + It("returns error for unknown file extension", func() { + tempFile, err := os.CreateTemp("", "test*.txt") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tempFile.Name()) + got, err := parseBackupData(tempFile.Name(), false, false, getLogger()) + Expect(err).To(HaveOccurred()) + Expect(got).To(BeNil()) + }) + }) + + Describe("getDataFromHistoryDB", func() { + It("returns error for invalid db file path", func() { + out := &bytes.Buffer{} + logger := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{Level: slog.LevelError})) + _, err := getDataFromHistoryDB("/nonexistent/path/to/db.db", false, false, logger) + Expect(err).To(HaveOccurred()) + Expect(out.String()).To(ContainSubstring("Get backups from history db failed")) + }) + It("returns error for corrupted db with invalid backup data", func() { + dbFile := createCorruptedDBFile() + defer os.Remove(dbFile) + out := &bytes.Buffer{} + logger := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{Level: slog.LevelError})) + _, err := getDataFromHistoryDB(dbFile, false, false, logger) + Expect(err).To(HaveOccurred()) + Expect(out.String()).To(ContainSubstring("Get backups from history db failed")) + }) + It("returns error for db with invalid backup name", func() { + dbFile := createDBWithInvalidBackupName() + defer os.Remove(dbFile) + out := &bytes.Buffer{} + logger := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{Level: slog.LevelError})) + _, err := getDataFromHistoryDB(dbFile, false, false, logger) + Expect(err).To(HaveOccurred()) + Expect(out.String()).To(ContainSubstring("Get backup data from history db failed")) + }) + }) +}) diff --git a/go.mod b/go.mod index cec552ff..c39faca4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.0 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/alecthomas/kingpin/v2 v2.4.0 github.com/apache/cloudberry-go-libs v1.0.12-0.20250910014224-fc376e8a1056 github.com/aws/aws-sdk-go v1.44.257 github.com/blang/semver v3.5.1+incompatible @@ -11,7 +12,7 @@ require ( github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf github.com/jackc/pgconn v1.14.3 github.com/jmoiron/sqlx v1.3.5 - github.com/klauspost/compress v1.15.15 + github.com/klauspost/compress v1.18.0 github.com/lib/pq v1.10.7 github.com/mattn/go-sqlite3 v1.14.19 github.com/nightlyone/lockfile v1.0.0 @@ -19,23 +20,32 @@ require ( github.com/onsi/ginkgo/v2 v2.13.0 github.com/onsi/gomega v1.27.10 github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/common v0.67.5 + github.com/prometheus/exporter-toolkit v0.15.1 github.com/sergi/go-diff v1.3.1 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/urfave/cli v1.22.13 - golang.org/x/sys v0.18.0 - golang.org/x/tools v0.12.0 + golang.org/x/sys v0.39.0 + golang.org/x/tools v0.39.0 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/yaml.v2 v2.4.0 ) require ( + github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.6.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/fatih/color v1.14.1 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect @@ -45,13 +55,27 @@ require ( github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgx/v4 v4.18.2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/mdlayher/vsock v1.2.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/text v0.14.0 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ca12ca24..9f21e075 100644 --- a/go.sum +++ b/go.sum @@ -4,14 +4,22 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20O github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/apache/cloudberry-go-libs v1.0.12-0.20250910014224-fc376e8a1056 h1:ycrFztmYATpidbSAU1rw60XuhuDxgBHtLD3Sueu947c= github.com/apache/cloudberry-go-libs v1.0.12-0.20250910014224-fc376e8a1056/go.mod h1:lfHWkNYsno/lV+Nee0OoCmlOlBz5yvT6EW8WQEOUI5c= github.com/aws/aws-sdk-go v1.44.257 h1:HwelXYZZ8c34uFFhgVw3ybu2gB5fkk8KLj2idTvzZb8= github.com/aws/aws-sdk-go v1.44.257/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/vfs v1.0.0 h1:AUZUgulCDzbaNjTRWEP45X7m/J10brAptZpSRKRZBZc= github.com/blang/vfs v1.0.0/go.mod h1:jjuNUc/IKcRNNWC9NUCvz4fR9PZLPIKxEygtPs/4tSI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -19,6 +27,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= @@ -39,13 +49,17 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -104,17 +118,23 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= -github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -137,6 +157,14 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= +github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -150,9 +178,21 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/exporter-toolkit v0.15.1 h1:XrGGr/qWl8Gd+pqJqTkNLww9eG8vR/CoRk0FubOKfLE= +github.com/prometheus/exporter-toolkit v0.15.1/go.mod h1:P/NR9qFRGbCFgpklyhix9F6v6fFr/VQB/CVsrMDGKo4= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -175,6 +215,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -183,16 +224,23 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli v1.22.13 h1:wsLILXG8qCJNse/qAgLNf23737Cx05GflHg/PJGe1Ok= github.com/urfave/cli v1.22.13/go.mod h1:VufqObjsMTF2BBwKawpx9R8eAneNEWhoO0yx8Vd+FkE= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -200,6 +248,8 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -210,14 +260,14 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -225,10 +275,14 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -246,8 +300,10 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= +golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -259,8 +315,10 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -271,19 +329,20 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/gpbackup_exporter.go b/gpbackup_exporter.go new file mode 100644 index 00000000..e08e0b7b --- /dev/null +++ b/gpbackup_exporter.go @@ -0,0 +1,172 @@ +//go:build gpbackup_exporter + +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package main + +import ( + "log/slog" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/alecthomas/kingpin/v2" + "github.com/apache/cloudberry-backup/exporter" + "github.com/prometheus/client_golang/prometheus" + version_collector "github.com/prometheus/client_golang/prometheus/collectors/version" + "github.com/prometheus/common/promslog" + "github.com/prometheus/common/promslog/flag" + "github.com/prometheus/common/version" + "github.com/prometheus/exporter-toolkit/web/kingpinflag" +) + +const exporterName = "gpbackup_exporter" + +func main() { + var ( + webPath = kingpin.Flag( + "web.telemetry-path", + "Path under which to expose metrics.", + ).Default("/metrics").String() + webAdditionalToolkitFlags = kingpinflag.AddFlags(kingpin.CommandLine, ":19854") + collectionInterval = kingpin.Flag( + "collect.interval", + "Collecting metrics interval in seconds.", + ).Default("600").Int() + collectionDepth = kingpin.Flag( + "collect.depth", + "Metrics depth collection in days. Metrics for backup older than this interval will not be collected. 0 - disable.", + ).Default("0").Int() + gpbckpHistoryFilePath = kingpin.Flag( + "gpbackup.history-file", + "Path to gpbackup_history.db.", + ).Default("").String() + gpbckpIncludeDB = kingpin.Flag( + "gpbackup.db-include", + "Specific db for collecting metrics. Can be specified several times.", + ).Default("").PlaceHolder("\"\"").Strings() + gpbckpExcludeDB = kingpin.Flag( + "gpbackup.db-exclude", + "Specific db to exclude from collecting metrics. Can be specified several times.", + ).Default("").PlaceHolder("\"\"").Strings() + gpbckpBackupType = kingpin.Flag( + "gpbackup.backup-type", + "Specific backup type for collecting metrics. One of: [full, incremental, data-only, metadata-only].", + ).Default("").String() + gpbckpBackupCollectDeleted = kingpin.Flag( + "gpbackup.collect-deleted", + "Collecting metrics for deleted backups.", + ).Default("false").Bool() + gpbckpBackupCollectFailed = kingpin.Flag( + "gpbackup.collect-failed", + "Collecting metrics for failed backups.", + ).Default("false").Bool() + ) + // Set logger config. + promslogConfig := &promslog.Config{} + // Add flags log.level and log.format from promslog package. + flag.AddFlags(kingpin.CommandLine, promslogConfig) + kingpin.Version(version.Print(exporterName)) + // Add short help flag. + kingpin.HelpFlag.Short('h') + // Load command line arguments. + kingpin.Parse() + // Setup signal catching. + sigs := make(chan os.Signal, 1) + // Catch listed signals. + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + // Set logger. + logger := promslog.New(promslogConfig) + // Method invoked upon seeing signal. + go func(logger *slog.Logger) { + s := <-sigs + logger.Warn( + "Stopping exporter", + "name", filepath.Base(os.Args[0]), + "signal", s) + os.Exit(1) + }(logger) + logger.Info( + "Starting exporter", + "name", filepath.Base(os.Args[0]), + "version", version.Info()) + logger.Info("Build context", "build_context", version.BuildContext()) + logger.Info( + "History database file path", + "file", *gpbckpHistoryFilePath) + logger.Info( + "Collecting metrics for deleted and failed backups", + "deleted", *gpbckpBackupCollectDeleted, + "failed", *gpbckpBackupCollectFailed, + ) + if *collectionDepth > 0 { + logger.Info( + "Metrics depth collection in days", + "depth", *collectionDepth) + } + if strings.Join(*gpbckpIncludeDB, "") != "" { + for _, db := range *gpbckpIncludeDB { + logger.Info( + "Collecting metrics for specific DB", + "DB", db) + } + } + if strings.Join(*gpbckpExcludeDB, "") != "" { + for _, db := range *gpbckpExcludeDB { + logger.Info( + "Exclude collecting metrics for specific DB", + "DB", db) + } + } + if *gpbckpBackupType != "" { + logger.Info( + "Collecting metrics for specific backup type", + "type", *gpbckpBackupType) + } + // Setup parameters for exporter. + exporter.SetPromPortAndPath(*webAdditionalToolkitFlags, *webPath) + logger.Info( + "Use exporter parameters", + "endpoint", *webPath, + "config.file", *webAdditionalToolkitFlags.WebConfigFile, + ) + // Exporter build info metric. + prometheus.MustRegister(version_collector.NewCollector(exporterName)) + // Start web server. + exporter.StartPromEndpoint(version.Info(), logger) + for { + // Get information from gpbackup_history.db. + exporter.GetGPBackupInfo( + *gpbckpHistoryFilePath, + *gpbckpBackupType, + *gpbckpBackupCollectDeleted, + *gpbckpBackupCollectFailed, + *gpbckpIncludeDB, + *gpbckpExcludeDB, + *collectionDepth, + logger, + ) + // Sleep for 'collection.interval' seconds. + time.Sleep(time.Duration(*collectionInterval) * time.Second) + } +} From 1bfb3d7e2ed46722d23a9f08e953d3f80371522c Mon Sep 17 00:00:00 2001 From: woblerr Date: Sun, 12 Apr 2026 17:07:18 +0300 Subject: [PATCH 02/12] Add end-to-end tests for gpbackup_exporter. Add e2e test that runs gpbackup commands and validates that gpbackup_exporter correctly reads history database and exposes metrics. Also remove dead gpbackmanPath assignment from useOldBackupVersion in e2e tests. And suppress noisy test logger output by writing to bytes.Buffer instead of os.Stdout in exporter unit tests. --- end_to_end/end_to_end_suite_test.go | 3 +- end_to_end/exporter_test.go | 143 ++++++++++++++++++++++++++++ exporter/gpbckp_parser_test.go | 2 +- 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 end_to_end/exporter_test.go diff --git a/end_to_end/end_to_end_suite_test.go b/end_to_end/end_to_end_suite_test.go index a9207097..a21a5977 100644 --- a/end_to_end/end_to_end_suite_test.go +++ b/end_to_end/end_to_end_suite_test.go @@ -71,6 +71,7 @@ var ( backupDir string segmentCount int gpbackmanPath string + exporterPath string ) const ( @@ -558,7 +559,6 @@ options: oldBackupVersionStr := os.Getenv("OLD_BACKUP_VERSION") _, restoreHelperPath, gprestorePath = buildAndInstallBinaries() - gpbackmanPath = fmt.Sprintf("%s/go/bin/gpbackman", operating.System.Getenv("HOME")) // Precompiled binaries will exist when running the ci job, `backward-compatibility` if _, err := os.Stat(fmt.Sprintf("/tmp/%s", oldBackupVersionStr)); err == nil { @@ -580,6 +580,7 @@ options: backupHelperPath = fmt.Sprintf("%s/gpbackup_helper", binDir) restoreHelperPath = backupHelperPath gpbackmanPath = fmt.Sprintf("%s/gpbackman", binDir) + exporterPath = fmt.Sprintf("%s/gpbackup_exporter", binDir) } segConfig := cluster.MustGetSegmentConfiguration(backupConn) backupCluster = cluster.NewCluster(segConfig) diff --git a/end_to_end/exporter_test.go b/end_to_end/exporter_test.go new file mode 100644 index 00000000..5e8697f3 --- /dev/null +++ b/end_to_end/exporter_test.go @@ -0,0 +1,143 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +package end_to_end_test + +import ( + "fmt" + "io" + "net" + "net/http" + "os" + "os/exec" + "regexp" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func getFreeTCPPort() int { + listener, err := net.Listen("tcp", "127.0.0.1:0") + Expect(err).NotTo(HaveOccurred()) + port := listener.Addr().(*net.TCPAddr).Port + listener.Close() + return port +} + +type exporterMetricCheck struct { + pattern string + count int +} + +func validateExporterMetrics(body string, checks []exporterMetricCheck) { + lines := strings.Split(body, "\n") + for _, em := range checks { + re := regexp.MustCompile(em.pattern) + count := 0 + for _, line := range lines { + if re.MatchString(line) { + count++ + } + } + Expect(count).To(Equal(em.count), + fmt.Sprintf("metric pattern %q: got %d, want %d", em.pattern, count, em.count)) + } +} + +func fetchMetrics(url string, client *http.Client) (string, error) { + if client == nil { + client = &http.Client{Timeout: 5 * time.Second} + } + resp, err := client.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + if resp.StatusCode != http.StatusOK { + return string(body), fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + return string(body), nil +} + +var _ = Describe("gpbackup_exporter end to end tests", func() { + BeforeEach(func() { + if useOldBackupVersion { + Skip("exporter tests are not applicable in old backup version mode") + } + if _, err := os.Stat(exporterPath); os.IsNotExist(err) { + Skip("gpbackup_exporter binary not found, skipping exporter e2e tests") + } + }) + + It("collects and exposes metrics without web config", func() { + expectedMetrics := []exporterMetricCheck{ + {`^gpbackup_backup_deletion_status\{`, 3}, + {`^gpbackup_backup_duration_seconds\{`, 3}, + {`^gpbackup_backup_info\{`, 3}, + {`^gpbackup_backup_status\{`, 3}, + {`^gpbackup_backup_status\{.*\} 0$`, 3}, + {`^gpbackup_backup_duration_seconds\{.*object_filtering="none",plugin="none".*\}`, 3}, + {`^gpbackup_backup_info\{.*database_name="testdb".*\}`, 3}, + {`^gpbackup_backup_since_last_completion_seconds\{`, 3}, + {`^gpbackup_backup_since_last_completion_seconds\{.*backup_type="full",database_name="testdb".*\}`, 1}, + {`^gpbackup_backup_since_last_completion_seconds\{.*backup_type="incremental",database_name="testdb".*\}`, 1}, + {`^gpbackup_backup_since_last_completion_seconds\{.*backup_type="metadata-only",database_name="testdb".*\}`, 1}, + {`^gpbackup_exporter_status\{database_name="testdb"\} 1$`, 1}, + {`^gpbackup_exporter_build_info\{.*\} 1$`, 1}, + } + end_to_end_setup() + defer end_to_end_teardown() + historyDB := getHistoryDBPathForCluster() + gpbackup(gpbackupPath, backupHelperPath, + "--backup-dir", backupDir, + "--leaf-partition-data") + gpbackup(gpbackupPath, backupHelperPath, + "--backup-dir", backupDir, + "--incremental", + "--leaf-partition-data") + gpbackup(gpbackupPath, backupHelperPath, + "--backup-dir", backupDir, + "--metadata-only") + port := getFreeTCPPort() + cmd := exec.Command(exporterPath, + "--gpbackup.history-file", historyDB, + "--collect.interval", "600", + "--web.listen-address", fmt.Sprintf("127.0.0.1:%d", port), + ) + cmd.Start() + defer cmd.Process.Kill() + metricsURL := fmt.Sprintf("http://127.0.0.1:%d/metrics", port) + var body string + Eventually(func() string { + b, err := fetchMetrics(metricsURL, nil) + if err != nil { + return "" + } + body = b + return b + }, 10*time.Second, 500*time.Millisecond).Should(ContainSubstring("gpbackup_backup_status")) + validateExporterMetrics(body, expectedMetrics) + }) +}) diff --git a/exporter/gpbckp_parser_test.go b/exporter/gpbckp_parser_test.go index 8e44a1bd..14c577c5 100644 --- a/exporter/gpbckp_parser_test.go +++ b/exporter/gpbckp_parser_test.go @@ -35,7 +35,7 @@ import ( ) func getLogger() *slog.Logger { - return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + return slog.New(slog.NewTextHandler(&bytes.Buffer{}, &slog.HandlerOptions{ Level: slog.LevelInfo, })) } From 3988154cce381056ae5fc80bb3dc193c48a24083 Mon Sep 17 00:00:00 2001 From: woblerr Date: Sun, 12 Apr 2026 17:48:39 +0300 Subject: [PATCH 03/12] Add README for gpbackup_exporter. --- exporter/README.md | 122 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 exporter/README.md diff --git a/exporter/README.md b/exporter/README.md new file mode 100644 index 00000000..2b1a448d --- /dev/null +++ b/exporter/README.md @@ -0,0 +1,122 @@ + + +# gpbackup_exporter + +**gpbackup_exporter** is a Prometheus exporter for collecting metrics from gpbackup history database (`gpbackup_history.db`). + +## Metrics +### Backup metrics +| Metric | Description | Labels | Additional Info | +| ----------- | ------------------ | ------------- | --------------- | +| `gpbackup_backup_status` | backup status | backup_type, database_name, object_filtering, plugin, timestamp | Values description:
`0` - success,
`1` - failure.| +| `gpbackup_backup_deletion_status` | backup deletion status | backup_type, database_name, date_deleted, object_filtering, plugin, timestamp | Values description:
`0` - backup still exists,
`1` - backup was successfully deleted,
`2` - the deletion is in progress,
`3` - last delete attempt failed to delete backup from plugin storage,
`4` - last delete attempt failed to delete backup from local storage.| +| `gpbackup_backup_info` | backup info | backup_dir, backup_ver, backup_type, compression_type, database_name, database_ver, object_filtering, plugin, plugin_ver, timestamp, with_statistic | Values description:
`1` - info about backup is exist.| +| `gpbackup_backup_duration_seconds` | backup duration in seconds| backup_type, database_name, end_time, object_filtering, plugin, timestamp || + +### Last backup metrics +| Metric | Description | Labels | Additional Info | +| ----------- | ------------------ | ------------- | --------------- | +| `gpbackup_backup_since_last_completion_seconds`| seconds since the last completed backup | backup_type, database_name || + +### Exporter metrics + +| Metric | Description | Labels | Additional Info | +| ----------- | ------------------ | ------------- | --------------- | +| `gpbackup_exporter_build_info` | information about gpbackup exporter | branch, goarch, goos, goversion, revision, tags, version | | +| `gpbackup_exporter_status` | gpbackup exporter get data status | database_name | Values description:
`0` - errors occurred when fetching information from history database,
`1` - information successfully fetched from history database. | + +## Getting Started +Available configuration flags: + +```bash +./gpbackup_exporter --help +usage: gpbackup_exporter [] + + +Flags: + -h, --[no-]help Show context-sensitive help (also try --help-long and --help-man). + --web.telemetry-path="/metrics" + Path under which to expose metrics. + --web.listen-address=:19854 ... + Addresses on which to expose metrics and web interface. Repeatable for multiple addresses. Examples: `:9100` or `[::1]:9100` for http, `vsock://:9100` for vsock + --web.config.file="" Path to configuration file that can enable TLS or authentication. See: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md + --collect.interval=600 Collecting metrics interval in seconds. + --collect.depth=0 Metrics depth collection in days. Metrics for backup older than this interval will not be collected. 0 - disable. + --gpbackup.history-file="" + Path to gpbackup_history.db. + --gpbackup.db-include="" ... + Specific db for collecting metrics. Can be specified several times. + --gpbackup.db-exclude="" ... + Specific db to exclude from collecting metrics. Can be specified several times. + --gpbackup.backup-type="" Specific backup type for collecting metrics. One of: [full, incremental, data-only, metadata-only]. + --[no-]gpbackup.collect-deleted + Collecting metrics for deleted backups. + --[no-]gpbackup.collect-failed + Collecting metrics for failed backups. + --log.level=info Only log messages with the given severity or above. One of: [debug, info, warn, error] + --log.format=logfmt Output format of log messages. One of: [logfmt, json] + --[no-]version Show application version. +``` + +### Additional description of flags. + +It's necessary to specify the `gpbackup_history.db` file location via `--gpbackup.history-file` flag. + +By default, metrics a collected only for active backups. The flag `--gpbackup.collect-deleted` allows to collect metrics for deleted backups. The flag `--gpbackup.collect-failed` allows to collect metrics for failed backups. + +Custom database for collecting metrics can be specified via `--gpbackup.db-include` flag. You can specify several databases.
+For example, `--gpbackup.db-include=demo1 --gpbackup.db-include=demo2`.
+For this case, metrics will be collected only for `demo1` and `demo2` databases. + +Custom database to exclude from collecting metrics can be specified via `--gpbackup.db-exclude` flag. You can specify several databases.
+For example, `--gpbackup.db-exclude=demo1 --gpbackup.db-exclude=demo2`.
+For this case, metrics **will not be collected** for `demo1` and `demo2` databases.
+If the same database is specified for include and exclude flags, then metrics for this database will not be collected. +The flag `--gpbackup.db-exclude` has a higher priority.
+For example, `--gpbackup.db-include=demo1 --gpbackup.db-exclude=demo1`.
+For this case, metrics **will not be collected** for `demo1` database. + +Custom `backup type` for collecting metrics can be specified via `--gpbackup.backup-type` flag. Valid values: `full`, `incremental`, `data-only`, `metadata-only`.
+For example, `--gpbackup.backup-type=full`.
+For this case, metrics will be collected only for `full` backups.
+ +Custom metrics depth collection in days can be specified via `--collect.depth` flag. Since gpbackup doesn't have regular options for removing info about outdated backups from history file, it is possible to limit the depth of collection metrics.
+For example, `--collect.depth=14`.
+For this case, metrics will be collected for backups not older then 14 days from current time.
+Value `0` - disable this functionality. + +When `--log.level=debug` is specified - information of values and labels for metrics is printing to the log. + +The flag `--web.config.file` allows to specify the path to the configuration for TLS and/or basic authentication. + +## Running + +```bash +./gpbackup_exporter \ + --gpbackup.history-file=/data/master/gpseg-1/gpbackup_history.db \ + --gpbackup.collect-deleted \ + --gpbackup.collect-failed +``` + +After starting, metrics are available at `http://localhost:19854/metrics`. + +## About + +gpbackup_exporter is part of the Apache Cloudberry Backup (Incubating) toolset. It is based on the original [gpbackup_exporter](https://github.com/woblerr/gpbackup_exporter) project. From ec786453da5e95811eea3ea0700cdfccbe07d014 Mon Sep 17 00:00:00 2001 From: woblerr Date: Sun, 12 Apr 2026 17:52:11 +0300 Subject: [PATCH 04/12] Add additional tools section to README. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 546bd802..d41b4b26 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,14 @@ gprestore --timestamp Run `--help` with either command for a complete list of options. +## Additional tools + +This repository also includes the following tools: + +* [gpbackup_s3_plugin](./plugins/s3plugin/README.md) — S3 storage plugin for gpbackup and gprestore. +* [gpBackMan](./gpbackman/README.md) — utility for managing backups created by gpbackup. +* [gpbackup_exporter](./exporter/README.md) — Prometheus exporter for collecting metrics from gpbackup history database. + ## Validation and code quality ### Test setup From 7abc3a77b91f2a5b4e4741af3af027ab8c01e46c Mon Sep 17 00:00:00 2001 From: woblerr Date: Sun, 12 Apr 2026 18:20:39 +0300 Subject: [PATCH 05/12] Add smoke tests for gpbackup_exporter in CI. --- .github/workflows/build_and_unit_test.yml | 3 +++ .github/workflows/cloudberry-backup-ci.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build_and_unit_test.yml b/.github/workflows/build_and_unit_test.yml index cb50de4f..ae140f9f 100644 --- a/.github/workflows/build_and_unit_test.yml +++ b/.github/workflows/build_and_unit_test.yml @@ -58,6 +58,9 @@ jobs: echo "=== Testing gpbackman ===" ${GOPATH}/bin/gpbackman --version ${GOPATH}/bin/gpbackman --help > /dev/null + echo "=== Testing gpbackup_exporter ===" + ${GOPATH}/bin/gpbackup_exporter --version + ${GOPATH}/bin/gpbackup_exporter --help > /dev/null echo "=== All smoke tests passed ===" - name: Unit Test diff --git a/.github/workflows/cloudberry-backup-ci.yml b/.github/workflows/cloudberry-backup-ci.yml index f6b72086..26abfb67 100644 --- a/.github/workflows/cloudberry-backup-ci.yml +++ b/.github/workflows/cloudberry-backup-ci.yml @@ -334,6 +334,9 @@ jobs: echo "=== Testing gpbackman ===" ${GPHOME}/bin/gpbackman --version ${GPHOME}/bin/gpbackman --help > /dev/null + echo "=== Testing gpbackup_exporter ===" + ${GPHOME}/bin/gpbackup_exporter --version + ${GPHOME}/bin/gpbackup_exporter --help > /dev/null echo "=== All smoke tests passed ===" | tee "${TEST_LOG_ROOT}/cloudberry-backup-smoke.log" ;; unit) From 127e2809481d3c2298bc24148d61f441639503f4 Mon Sep 17 00:00:00 2001 From: woblerr Date: Mon, 13 Apr 2026 15:27:38 +0300 Subject: [PATCH 06/12] Fix smoke test for gpbackup_exporter in install target. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3bab4d81..fa27092c 100644 --- a/Makefile +++ b/Makefile @@ -114,7 +114,7 @@ build_linux : env GOOS=linux GOARCH=amd64 $(GO_BUILD) -tags '$(EXPORTER)' -o $(EXPORTER) -ldflags "$(EXPORTER_VERSION_STR)" install : - cp $(BIN_DIR)/$(BACKUP) $(BIN_DIR)/$(RESTORE) $(BIN_DIR)/$(GPBACKMAN) $(GPHOME)/bin + cp $(BIN_DIR)/$(BACKUP) $(BIN_DIR)/$(RESTORE) $(BIN_DIR)/$(GPBACKMAN) $(BIN_DIR)/$(EXPORTER) $(GPHOME)/bin @psql -X -t -d template1 -c 'select distinct hostname from gp_segment_configuration where content != -1' > /tmp/seg_hosts 2>/dev/null; \ if [ $$? -eq 0 ]; then \ $(COPYUTIL) -f /tmp/seg_hosts $(helper_path) $(s3plugin_path) =:$(GPHOME)/bin/; \ From a3dc75dcd6a5b52f2849566c6caa3e7babc29b33 Mon Sep 17 00:00:00 2001 From: woblerr Date: Fri, 24 Apr 2026 23:28:52 +0300 Subject: [PATCH 07/12] Graceful shutdown with exit status 0. SIGINT means "interrupt," signaling a user wants to stop the current operation. SIGTERM means "terminate," requesting a polite program shutdown. So' it's correct to consider as graceful shutdown --- gpbackup_exporter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gpbackup_exporter.go b/gpbackup_exporter.go index e08e0b7b..fc9f9da3 100644 --- a/gpbackup_exporter.go +++ b/gpbackup_exporter.go @@ -100,11 +100,11 @@ func main() { // Method invoked upon seeing signal. go func(logger *slog.Logger) { s := <-sigs - logger.Warn( + logger.Info( "Stopping exporter", "name", filepath.Base(os.Args[0]), "signal", s) - os.Exit(1) + os.Exit(0) }(logger) logger.Info( "Starting exporter", From e7624ade746463776fa0b50762c11e2df0785eb6 Mon Sep 17 00:00:00 2001 From: woblerr Date: Fri, 24 Apr 2026 23:33:13 +0300 Subject: [PATCH 08/12] Fix port in test. --- exporter/gpbckp_exporter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/gpbckp_exporter_test.go b/exporter/gpbckp_exporter_test.go index 0212c05d..2b908e10 100644 --- a/exporter/gpbckp_exporter_test.go +++ b/exporter/gpbckp_exporter_test.go @@ -57,7 +57,7 @@ var _ = Describe("Exporter", func() { Describe("SetPromPortAndPath", func() { It("sets web flags config and endpoint", func() { testFlagsConfig := web.FlagConfig{ - WebListenAddresses: &([]string{":9854"}), + WebListenAddresses: &([]string{":19854"}), WebSystemdSocket: func(i bool) *bool { return &i }(false), WebConfigFile: func(i string) *string { return &i }(""), } From f64b6b5fbea18b9bce7a729dacbe7575e233f839 Mon Sep 17 00:00:00 2001 From: woblerr Date: Sat, 25 Apr 2026 00:03:55 +0300 Subject: [PATCH 09/12] Prevent panic and make exit. To prevent a panic with an error like "panic: http: invalid pattern", exit in case of an empty endpoint. --- exporter/gpbckp_exporter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exporter/gpbckp_exporter.go b/exporter/gpbckp_exporter.go index d384f8a9..06bc6e9a 100644 --- a/exporter/gpbckp_exporter.go +++ b/exporter/gpbckp_exporter.go @@ -46,7 +46,8 @@ func SetPromPortAndPath(flagsConfig web.FlagConfig, endpoint string) { func StartPromEndpoint(version string, logger *slog.Logger) { go func(logger *slog.Logger) { if webEndpoint == "" { - logger.Error("Metric endpoint is empty", "endpoint", webEndpoint) + logger.Error("Metric endpoint is empty; aborting exporter startup", "endpoint", webEndpoint) + os.Exit(1) } http.Handle(webEndpoint, promhttp.Handler()) if webEndpoint != "/" { From e9d1aa2d948fea76e865a5a470ab3c7ee9c97161 Mon Sep 17 00:00:00 2001 From: woblerr Date: Wed, 29 Apr 2026 21:03:13 +0300 Subject: [PATCH 10/12] Remove unused getDataSuccessStatus and simplify GetGPBackupInfo. Remove unused getDataSuccessStatus. Clarify include/exclude handling: * DB present in both dbInclude and dbExclude - warn and emit gpbackup_exporter_status=0. * DB only in dbExclude - skip, emit no metrics. * dbInclude empty or DB in dbInclude - process and emit full backup metrics. Add unit tests. --- exporter/gpbckp_exporter.go | 13 +++----- exporter/gpbckp_exporter_test.go | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/exporter/gpbckp_exporter.go b/exporter/gpbckp_exporter.go index 06bc6e9a..de505786 100644 --- a/exporter/gpbckp_exporter.go +++ b/exporter/gpbckp_exporter.go @@ -83,9 +83,8 @@ func StartPromEndpoint(version string, logger *slog.Logger) { // GetGPBackupInfo get and parse gpbackup history database. func GetGPBackupInfo(historyFile, backupType string, collectDeleted, collectFailed bool, dbInclude, dbExclude []string, collectDepth int, logger *slog.Logger) { - // The flag indicates whether it was possible to get data from the gpbackup history. - // By default, it's set to true. - getDataSuccessStatus := true + // To calculate the time elapsed since the last completed backup for specific database. + // For all databases values are calculated relative to one value. // To calculate the time elapsed since the last completed backup for specific database. // For all databases values are calculated relative to one value. currentTime := time.Now() @@ -97,7 +96,6 @@ func GetGPBackupInfo(historyFile, backupType string, collectDeleted, collectFail backupConfigs, err := parseBackupData(historyFile, collectDeleted, collectFailed, logger) if err != nil { logger.Error("Get data failed", "err", err) - getDataSuccessStatus = false } // Reset metrics. resetMetrics() @@ -111,7 +109,7 @@ func GetGPBackupInfo(historyFile, backupType string, collectDeleted, collectFail // then metrics for this database will not be collected. if !dbInList(db, dbExclude) { if listEmpty(dbInclude) || dbInList(db, dbInclude) { - dbStatus[db] = getDataSuccessStatus + dbStatus[db] = true bckpType, err := gpbckpconfig.GetBackupType(backupConfigs[i]) if err != nil { logger.Error("Parse backup type value failed", "err", err) @@ -171,9 +169,8 @@ func GetGPBackupInfo(historyFile, backupType string, collectDeleted, collectFail } else if dbInList(db, dbInclude) { // When db is specified in both include and exclude lists, a warning is displayed in the log // and data for this db is not collected. - // It is necessary to set zero metric value for this db. - getDataSuccessStatus = false - dbStatus[db] = getDataSuccessStatus + // Set zero metric value for this db. + dbStatus[db] = false logger.Warn("DB is specified in include and exclude lists", "DB", db) } } diff --git a/exporter/gpbckp_exporter_test.go b/exporter/gpbckp_exporter_test.go index 2b908e10..c4016afc 100644 --- a/exporter/gpbckp_exporter_test.go +++ b/exporter/gpbckp_exporter_test.go @@ -153,6 +153,28 @@ var _ = Describe("Exporter", func() { Expect(out.String()).To(ContainSubstring(`No backup data returned`)) }) + It("logs parse error and emits no metrics", func() { + tempFile, err := os.CreateTemp("", "test*.yaml") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tempFile.Name()) + resetMetrics() + out := &bytes.Buffer{} + lc := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) + GetGPBackupInfo(tempFile.Name(), "", false, false, []string{""}, []string{""}, 0, lc) + logOutput := out.String() + Expect(logOutput).To(ContainSubstring("Get data failed")) + Expect(logOutput).To(ContainSubstring("No backup data returned")) + Expect(logOutput).ToNot(ContainSubstring("Set up metric")) + }) + It("warns when using depth and backup is older than depth interval", func() { backupConfigs := []*history.BackupConfig{templateBackupConfig()} dbFile, err := fakeHistoryFileData(backupConfigs) @@ -194,6 +216,39 @@ var _ = Describe("Exporter", func() { Expect(out.String()).To(ContainSubstring(`DB=test`)) }) + It("sets exporter status metric only for conflicting DB", func() { + goodConfig := templateBackupConfig() + goodConfig.DatabaseName = "good" + goodConfig.Timestamp = "20230118152654" + goodConfig.EndTime = "20230118152656" + + badConfig := templateBackupConfig() + badConfig.DatabaseName = "bad" + badConfig.Timestamp = "20230118152655" + badConfig.EndTime = "20230118152657" + + backupConfigs := []*history.BackupConfig{goodConfig, badConfig} + dbFile, err := fakeHistoryFileData(backupConfigs) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(dbFile) + resetMetrics() + out := &bytes.Buffer{} + lc := slog.New(slog.NewTextHandler(out, &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) + // Include both good and bad, but exclude only bad -> conflict for bad + GetGPBackupInfo(dbFile, "", false, false, []string{"good", "bad"}, []string{"bad"}, 0, lc) + logOutput := out.String() + Expect(logOutput).To(ContainSubstring(`metric=gpbackup_exporter_status value=1 labels=good`)) + Expect(logOutput).To(ContainSubstring(`metric=gpbackup_exporter_status value=0 labels=bad`)) + }) + It("logs errors for invalid backup values", func() { invalidConfig := &history.BackupConfig{ DatabaseName: "test", From 96d488620cd15f5c52df4e9c63236edbf4187043 Mon Sep 17 00:00:00 2001 From: woblerr Date: Wed, 29 Apr 2026 22:45:29 +0300 Subject: [PATCH 11/12] Ensure command starts successfully in gpbackup_exporter end-to-end tests. --- end_to_end/exporter_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end_to_end/exporter_test.go b/end_to_end/exporter_test.go index 5e8697f3..37ecc761 100644 --- a/end_to_end/exporter_test.go +++ b/end_to_end/exporter_test.go @@ -126,7 +126,7 @@ var _ = Describe("gpbackup_exporter end to end tests", func() { "--collect.interval", "600", "--web.listen-address", fmt.Sprintf("127.0.0.1:%d", port), ) - cmd.Start() + Expect(cmd.Start()).To(Succeed()) defer cmd.Process.Kill() metricsURL := fmt.Sprintf("http://127.0.0.1:%d/metrics", port) var body string From 39a950b54384c6ddb5f1284dd9e682a0ee60f517 Mon Sep 17 00:00:00 2001 From: woblerr Date: Thu, 30 Apr 2026 00:08:16 +0300 Subject: [PATCH 12/12] Update gpbackup_exporter_status metric description. --- exporter/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporter/README.md b/exporter/README.md index 2b1a448d..640cb192 100644 --- a/exporter/README.md +++ b/exporter/README.md @@ -40,7 +40,7 @@ | Metric | Description | Labels | Additional Info | | ----------- | ------------------ | ------------- | --------------- | | `gpbackup_exporter_build_info` | information about gpbackup exporter | branch, goarch, goos, goversion, revision, tags, version | | -| `gpbackup_exporter_status` | gpbackup exporter get data status | database_name | Values description:
`0` - errors occurred when fetching information from history database,
`1` - information successfully fetched from history database. | +| `gpbackup_exporter_status` | gpbackup exporter data collection status for a specific database | database_name | Values description:
`0` — exporter marked this database as not collected,
`1` — information successfully fetched from history database.| ## Getting Started Available configuration flags: