This is an automated email from the ASF dual-hosted git repository.

tuhaihe pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudberry-backup.git


The following commit(s) were added to refs/heads/main by this push:
     new 253159b7 Add gpbackup_exporter — a Prometheus exporter for gpbackup 
history database. (#87)
253159b7 is described below

commit 253159b7da96c927e2585a432c9d8e2d7a14395a
Author: Anton Kurochkin <[email protected]>
AuthorDate: Tue May 12 08:33:06 2026 +0300

    Add gpbackup_exporter — a Prometheus exporter for gpbackup history 
database. (#87)
    
    * 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.
    
    * 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.
    
    * Add README for gpbackup_exporter.
    
    * Add additional tools section to README.
    
    * Add smoke tests for gpbackup_exporter in CI.
    
    * Fix smoke test for gpbackup_exporter in install target.
    
    * 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
    
    * Fix port in test.
    
    * Prevent panic and make exit.
    To prevent a panic with an error like "panic: http: invalid pattern",  exit 
in case of an empty endpoint.
    
    * 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.
    
    * Ensure command starts successfully in gpbackup_exporter end-to-end tests.
    
    * Update gpbackup_exporter_status metric description.
    
    * Fix expected error message in exporter test for missing history DB.
    This aligns the test with the recent behavior change in commit 1035fb84.
    `OpenHistoryDB()` now fails fast on missing files during the open stage,
    rather than failing later during the first SQL query.
---
 .github/workflows/build_and_unit_test.yml   |   3 +
 .github/workflows/cloudberry-backup-ci.yml  |   3 +
 Makefile                                    |  16 +-
 README.md                                   |   8 +
 end_to_end/end_to_end_suite_test.go         |   3 +-
 end_to_end/exporter_test.go                 | 143 ++++++++++++++
 exporter/README.md                          | 122 ++++++++++++
 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                 | 186 ++++++++++++++++++
 exporter/gpbckp_exporter_metrics.go         |  52 +++++
 exporter/gpbckp_exporter_test.go            | 286 ++++++++++++++++++++++++++++
 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 +++++++++++++++++
 22 files changed, 2083 insertions(+), 34 deletions(-)

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)
diff --git a/Makefile b/Makefile
index 12b5fd44..fa27092c 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,9 +111,10 @@ 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
+               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/; \
@@ -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/README.md b/README.md
index 546bd802..d41b4b26 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,14 @@ gprestore --timestamp <YYYYMMDDHHMMSS>
 
 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
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..37ecc761
--- /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),
+               )
+               Expect(cmd.Start()).To(Succeed())
+               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/README.md b/exporter/README.md
new file mode 100644
index 00000000..640cb192
--- /dev/null
+++ b/exporter/README.md
@@ -0,0 +1,122 @@
+<!--
+  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.
+-->
+
+# 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:<br> `0` - 
success,<br> `1` - failure.|
+| `gpbackup_backup_deletion_status` | backup deletion status | backup_type, 
database_name, date_deleted, object_filtering, plugin, timestamp | Values 
description:<br> `0` - backup still exists,<br> `1` - backup was successfully 
deleted,<br> `2` - the deletion is in progress,<br> `3` - last delete attempt 
failed to delete backup from plugin storage,<br> `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:<br> `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 data collection status for a 
specific database | database_name | Values description:<br> `0` — exporter 
marked this database as not collected,<br> `1` — information successfully 
fetched from history database.|
+
+## Getting Started
+Available configuration flags:
+
+```bash
+./gpbackup_exporter --help
+usage: gpbackup_exporter [<flags>]
+
+
+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.<br>
+For example, `--gpbackup.db-include=demo1 --gpbackup.db-include=demo2`.<br>
+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.<br>
+For example, `--gpbackup.db-exclude=demo1 --gpbackup.db-exclude=demo2`.<br>
+For this case, metrics **will not be collected** for `demo1` and `demo2` 
databases.<br>
+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.<br>
+For example, `--gpbackup.db-include=demo1 --gpbackup.db-exclude=demo1`.<br>
+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`.<br>
+For example, `--gpbackup.backup-type=full`.<br>
+For this case, metrics will be collected only for `full` backups.<br>
+
+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.<br>
+For example, `--collect.depth=14`.<br> 
+For this case, metrics will be collected for backups not older then 14 days 
from current time.<br>
+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.
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..de505786
--- /dev/null
+++ b/exporter/gpbckp_exporter.go
@@ -0,0 +1,186 @@
+/*
+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; aborting 
exporter startup", "endpoint", webEndpoint)
+                       os.Exit(1)
+               }
+               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) {
+       // 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()
+       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)
+       }
+       // 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] = true
+                                       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.
+                               // Set zero metric value for this db.
+                               dbStatus[db] = false
+                               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..c4016afc
--- /dev/null
+++ b/exporter/gpbckp_exporter_test.go
@@ -0,0 +1,286 @@
+/*
+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{":19854"}),
+                               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("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)
+                       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("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",
+                               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..d7cacb71
--- /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(&bytes.Buffer{}, 
&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("Open gpbackup 
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..fc9f9da3
--- /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.Info(
+                       "Stopping exporter",
+                       "name", filepath.Base(os.Args[0]),
+                       "signal", s)
+               os.Exit(0)
+       }(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)
+       }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to