Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions docs/guides/postgres/remote-replica/monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
---
title: Monitoring PostgreSQL Remote Replicas
menu:
docs_{{ .version }}:
identifier: pg-remote-replica-monitoring
name: Monitoring
parent: pg-remote-replica
weight: 50
menu_name: docs_{{ .version }}
section_menu_id: guides
---

> New to KubeDB? Please start [here](/docs/README.md).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use descriptive link text.

Replace [here] with text that identifies the destination, such as [the KubeDB documentation overview]. This improves link context and resolves MD059.

🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 13-13: Link text should be descriptive

(MD059, descriptive-link-text)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guides/postgres/remote-replica/monitoring.md` at line 13, Update the
introductory link in the monitoring guide to replace the generic “here” text
with descriptive text identifying the KubeDB documentation overview, while
preserving the existing destination URL.

Source: Linters/SAST tools


# Monitoring PostgreSQL Remote Replicas

A remote replica is disaster-recovery infrastructure: the questions its monitoring must
answer are *is the replica streaming*, *can it see its source*, *how much data is at risk
if the source data center is lost right now* (RPO), and *did self-healing fire*. This
guide wires those up on the **replica-side cluster** and installs a Grafana dashboard
built around exactly those questions.

## What serves the metrics

Every remote replica pod runs a `pg-coordinator` sidecar in remote-replica mode. Besides
recovery and role-label management, it serves DR metrics on the `raft-metrics` port
(23790), fed by its own monitor and lag-monitor loops — Prometheus scrapes never touch
the source database:

| Metric | Meaning |
|---|---|
| `pg_coordinator_remote_replica_lag_bytes` | WAL bytes the source has written beyond what this pod has replayed — the data at risk (RPO). Absent until the first measurement |
| `pg_coordinator_remote_replica_streaming` | 1 when the source confirms this pod in `pg_stat_replication`; mirrors the pod's `standby` role label |
| `pg_coordinator_remote_replica_source_reachable` | 1 when the last source query succeeded — a DR replica that cannot see its source is not protecting anything |
| `pg_coordinator_remote_replica_last_lag_check_timestamp_seconds` | when the lag was last measured; the monitor backs off to 300s while in sync, so up to ~5 min of age is normal |
| `pg_coordinator_remote_replica_recovery_total{action,result}` | pg_rewind / pg_basebackup self-healing attempts; all four series exported from start, so any step above 0 is a real event |

The standard `postgres_exporter` (port 56790, added by `spec.monitor`) contributes
`pg_replication_is_replica`, `pg_replication_lag_seconds`, `pg_stat_activity_count`, etc.

## Prerequisites

On the replica-side cluster:

- [kube-prometheus-stack](https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack).
The dashboard below is developed against **Grafana 7.5.x** (`--set grafana.image.tag=7.5.5`).
- [Panopticon](https://appscode.com/products/panopticon/) with your KubeDB license — it
exports `kubedb_com_postgres_info`, which drives the dashboard's `app` variable.
- The `kubedb-metrics` chart (MetricsConfigurations for Panopticon).

## Step 1: enable monitoring on the remote replica

Add `spec.monitor` to the remote replica Postgres (the `release: prometheus` label must
match your kube-prometheus-stack release name — Prometheus only selects ServiceMonitors
carrying it):

```yaml
spec:
monitor:
agent: prometheus.io/operator
prometheus:
serviceMonitor:
labels:
release: prometheus
interval: 30s
```

```bash
kubectl patch pg pg-london -n demo --type merge -p '{
"spec": {"monitor": {"agent": "prometheus.io/operator",
"prometheus": {"serviceMonitor": {"labels": {"release": "prometheus"}, "interval": "30s"}}}}}'
```

This adds the exporter container to the pod template and creates the `<db>-stats`
Service + ServiceMonitor exposing **both** metric ports (`metrics`/56790 and
`raft-metrics`/23790).

> **The pod must be restarted once**: remote replica PetSets use the `OnDelete` update
> strategy, so the exporter container only appears after a pod delete. Streaming resumes
> automatically after the restart.

```bash
kubectl delete pod pg-london-0 -n demo
kubectl wait pg pg-london -n demo --for=jsonpath='{.status.phase}'=Ready --timeout=300s
kubectl get pod pg-london-0 -n demo -o jsonpath='{range .spec.containers[*]}{.name} {end}'
# postgres pg-coordinator exporter
```

## Step 2: if the cluster runs NetworkPolicies, allow the scrape

KubeDB can deploy NetworkPolicies that restrict ingress to database pods to their own
namespace (plus the operator). Prometheus lives in another namespace, so **both scrape
targets stay down** until you allow it. The symptom: `up{job="<db>-stats"} == 0` while
`wget 127.0.0.1:56790/metrics` inside the pod works fine.

```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus-scrape
namespace: demo
spec:
podSelector:
matchLabels:
app.kubernetes.io/component: database
app.kubernetes.io/managed-by: kubedb.com
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- port: 56790
protocol: TCP
- port: 23790
protocol: TCP
```

Skip this step if `kubectl get networkpolicy -n demo` shows nothing — whether KubeDB
creates these policies is an install-time choice (`networkPolicy.enabled` in the chart
values). On a cluster **without** them, do **not** apply this policy: it would become the
only policy selecting the database pods and deny all other ingress — operator health
checks fail and the CR sticks in `Provisioning`.

## Step 3: verify the scrape

```bash
PROM=prometheus-prometheus-kube-prometheus-prometheus-0
kubectl exec -n monitoring $PROM -c prometheus -- promtool query instant \
http://localhost:9090 'up{namespace="demo",pod=~"pg-london-.*"}'
# both endpoints (metrics and raft-metrics) must be 1

kubectl exec -n monitoring $PROM -c prometheus -- promtool query instant \
http://localhost:9090 'pg_coordinator_remote_replica_streaming{namespace="demo"}'
# 1 per streaming pod
```

## Step 4: install the dashboard

The **KubeDB / Postgres / Remote Replica** dashboard
([opnpulse/dashboards, postgres folder](https://github.com/opnpulse/dashboards/tree/master/postgres))
has three rows: *DR Protection Status* (streaming, source reachable, RPO in bytes, lag
data age, recoveries in 24h), *Replication Lag* (byte lag from the coordinator; apply
lag in seconds from the exporter — the latter also grows while the source is idle, read
them together), and *Self-Healing & Replica Health*.

Provision it as a ConfigMap so it survives Grafana restarts (kube-prometheus-stack's
Grafana has no persistence — dashboards imported through the UI or API are lost on pod
restart; the sidecar re-provisions labeled ConfigMaps):

```bash
kubectl create configmap pg-remote-replica-dashboard -n monitoring \
--from-file=postgres_remote_replica_dashboard.json
kubectl label configmap pg-remote-replica-dashboard -n monitoring grafana_dashboard=1
Comment on lines +142 to +156

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file around affected lines =="
if [ -f docs/guides/postgres/remote-replica/monitoring.md ]; then
  sed -n '35,60p;130,160p' docs/guides/postgres/remote-replica/monitoring.md | cat -n
else
  echo "target file not found"
fi

echo
echo "== dashboard json references in target file =="
rg -n "postgres_remote_replica_dashboard|external|dashboard|Grafana|7\.5|7\.4|github.com/opnpulse/dashboards" docs/guides/postgres/remote-replica/monitoring.md || true

echo
echo "== check public file listing and README version for referenced path =="
url="https://api.github.com/repos/opnpulse/dashboards/git/trees/master?recursive=1"
python3 - <<'PY'
import json, urllib.request
from urllib.request import Request, HTTPError
url="https://api.github.com/repos/opnpulse/dashboards/git/trees/master?recursive=1"
req=Request(url, headers={'User-Agent':'CodeRabbit'})
try:
    with urllib.request.urlopen(req, timeout=20) as r:
        data=json.load(r)
except Exception as e:
    print("GITHUB_TREE_ERROR:", type(e).__name__, e)
    raise SystemExit(0)
target="postgres_remote_replica_dashboard.json"
matches=[]
paths=[]
for item in data.get("tree", []):
    paths.append(item["path"])
    if item["path"] == target:
        matches.append(item)
print("master_contains_target_file:", len(matches)>0, matches[:5])
postgres_files=sorted(p for p in paths if p.startswith("postgres/") and p.endswith(".json"))
print("postgres_json_files=", postgres_files)
# find README line possibly mentioning versions
readme="".join(data.get("tree", [])[0]) if any(p=="README.md" for p in paths) else None
</PY

python3 - <<'PY'
import urllib.request
url="https://raw.githubusercontent.com/opnpulse/dashboards/master/README.md"
print("README fetch:", end=" ")
try:
    text=urllib.request.urlopen(url, timeout=20).read().decode()
    print("ok")
    for i,line in enumerate(text.splitlines(),1):
        if "Grafana" in line or "7.4" in line or "7.5" in line:
            print(f"{i}: {line[:200]}")
except Exception as e:
    print("error:", e)
PY

Repository: kubedb/docs

Length of output: 4426


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import urllib.request
import json

url = "https://api.github.com/repos/opnpulse/dashboards/git/trees/master?recursive=1"
req = urllib.request.Request(url, headers={"User-Agent": "CodeRabbit"})
try:
    with urllib.request.urlopen(req, timeout=20) as r:
        data = json.load(r)
except Exception as e:
    print("GITHUB_TREE_ERROR:", type(e).__name__, e)
    raise SystemExit(0)

target = "postgres_remote_replica_dashboard.json"
paths = [item["path"] for item in data.get("tree", [])]
print("master_contains_target_file:", target in paths)
print("matched_files:")
for item in data.get("tree", []):
    if item["path"] == target:
        print(item)

print("\npostgres_json_files:")
for p in sorted(p for p in paths if p.startswith("postgres/") and p.endswith(".json")):
    print(p)

readme_content = None
for item in data.get("tree", []):
    if item["path"] == "README.md" and item["type"] == "blob":
        with urllib.request.urlopen(item["url"], timeout=20) as r:
            readme_content = r.read().decode(errors="replace")
        break

print("\nGrafana/version mentions in README.md:")
if readme_content:
    for i, line in enumerate(readme_content.splitlines(), 1):
        if "Grafana" in line or "7.4" in line or "7.5" in line:
            print(f"{i}: {line}")
else:
    print("README.md not found")
PY

Repository: kubedb/docs

Length of output: 312


🌐 Web query:

github.com opnpulse dashboards master postgres postgres_remote_replica_dashboard.json

💡 Result:

The requested file, postgres_remote_replica_dashboard.json, does not appear to exist within the public repositories of the OpenPulse (opnpulse) GitHub organization [1]. While OpenPulse is an OpenTelemetry-based observability project maintained by AppsCode [1][2], public repositories associated with it, such as opnpulse/installer [3] and opnpulse/alerts [4], focus on infrastructure components, Helm charts [5][6], and runbooks [7], rather than providing a library of specific Grafana dashboard JSON files for PostgreSQL. It is possible the file you are looking for originated in a different repository, such as the AppsCode grafana-dashboards repository, which does contain various PostgreSQL-related dashboards [8][9][10], or it may be a custom or non-public file. For PostgreSQL observability with Prometheus and Grafana, the standard community source for such dashboards is the prometheus-community/postgres_exporter repository [11], which provides officially maintained dashboard templates.

Citations:


Use a dashboard source that contains postgres_remote_replica_dashboard.json.

opnpulse/dashboards@master/postgres does not include the referenced remote-replica JSON file, so the kubectl create configmap --from-file=postgres_remote_replica_dashboard.json command cannot run as written. Patch this step to include/checkout the artifact from a valid revision, or link a source where the file exists.

📍 Affects 1 file
  • docs/guides/postgres/remote-replica/monitoring.md#L138-L152 (this comment)
  • docs/guides/postgres/remote-replica/monitoring.md#L45-L49
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guides/postgres/remote-replica/monitoring.md` around lines 138 - 152,
The dashboard setup instructions at
docs/guides/postgres/remote-replica/monitoring.md lines 138-152 must use a valid
dashboard source containing postgres_remote_replica_dashboard.json; update the
referenced opnpulse/dashboards revision or link/provide checkout steps for a
source where the artifact exists, and ensure the kubectl command is runnable.
The related reference at docs/guides/postgres/remote-replica/monitoring.md lines
45-49 requires the same source correction; no separate behavior change is
needed.

```

Then open Grafana and select your namespace and database in the `namespace` / `app`
variables:

```bash
kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80
```

## Reading the dashboard

| Symptom | Likely meaning |
|---|---|
| Streaming red, Source Reachable green | a replica pod is down, diverged, or mid-recovery; watch Recovery Actions |
| Source Reachable red | the clusters are partitioned or the source is down — severity-1 even though the replica looks healthy locally |
| RPO climbing, Streaming green | WAL arrives but replay cannot keep up (or replay is paused) |
| Apply Lag climbing, byte lag 0 | the source is idle; not an incident |
| Recoveries ≥ 1 | self-healing fired (pg_rewind or re-seed) — read the coordinator logs of the affected pod |

## Next Steps

- [Remote Replica overview](/docs/guides/postgres/remote-replica/remotereplica.md)
- [Cross-Cluster DR with Bidirectional Failover](/docs/guides/postgres/remote-replica/advanced-setup.md)
- [Migration from Self-Managed PostgreSQL](/docs/guides/postgres/remote-replica/migration.md)
83 changes: 80 additions & 3 deletions docs/guides/postgres/remote-replica/remotereplica.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,74 @@ pg-singapore nginx pg-singapore.something.org 172.104.37.147 80
```

# Prepare for Remote Replica
We wil use the [kubedb_plugin](/docs/setup/README.md) for generating configuration for remote replica. It will create the appbinding and necessary secrets to connect with source server
We will use the [kubedb_plugin](/docs/setup/README.md) for generating configuration for remote replica. It creates the AppBinding and the secrets the replica needs to connect to the source server:

```bash
$ kubectl dba remote-config postgres -n demo pg-singapore -uremote -ppass -d 172.104.37.147 -y
home/mehedi/go/src/kubedb.dev/yamls/postgres/pg-singapore-remote-config.yaml
$ kubectl dba remote-config postgres -n demo pg-singapore \
-uremote -ppass \
-d 172.104.37.147:5432 \
--replica-name pg-london \
-y
kubectl apply -f /home/user/pg-singapore-remote-config.yaml
```

- `-d` takes the address the source is reachable on **from the replica's cluster** — a load
balancer frontend, not the in-cluster service. A non-standard port can be given as
`-d host:port` or with `--port` (it is written into the generated AppBinding's
`spec.clientConfig.service.port` and honored by the replica for the seed, streaming and
monitoring connections).
- `--replica-name` additionally emits a ready-to-apply remote replica `Postgres` manifest,
sized from the source's spec (version, replicas, storage, resources) with the
`remoteReplica` stanza and auth secret filled in. Treat it as a starting point — a
secondary site is often sized differently on purpose.
- `--auth-secret <name>` overrides the generated auth secret's name.
- The output file is always written to the **current directory** as
`<dbname>-remote-config.yaml`; `-y` only skips the confirmation prompt.
Comment on lines +218 to +224

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the duplicate pg-london creation path.

--replica-name is documented to emit a ready-to-apply Postgres manifest, and Line 210 applies the generated file. The later steps still create pg-london from the hand-written manifest at Lines 302-337 and create pg-london-auth at Lines 283-300. Following the documented sequence can hit AlreadyExists or apply a different auth configuration. Remove the later manual resources, or state that users must not apply the generated Postgres manifest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guides/postgres/remote-replica/remotereplica.md` around lines 218 - 224,
Remove the later manual creation steps for pg-london-auth and pg-london from the
documented sequence, including the hand-written manifest and apply commands
around the resource-creation sections. Keep the --replica-name generated
manifest flow as the sole resource-creation path, so applying the generated file
does not duplicate resources or override its authentication configuration.

- These flags require the kubectl-dba version shipped with this release or newer.


# TLS across clusters: sslmode and certificate SANs

The generated AppBinding inherits its `sslmode` from the source database's `spec.sslMode`.
Two things matter when the replica connects through a load balancer or any external
endpoint:

- **`verify-ca` is the recommended mode between clusters.** It verifies the server
certificate against your private CA (pinned via the exported `ca.crt`) without checking
the hostname — which is what you want when the same database is reached through
different names inside and outside its cluster.
- **`verify-full` additionally requires the exact hostname you dial to be present in the
server certificate's SAN list.** KubeDB issues the server certificate for the in-cluster
names, so a `verify-full` connection to an external address fails during the TLS
handshake unless that address was added to the certificate. The replica then loops
forever with `Attempting pg_isready on primary` while a plain (non-TLS) connection works
— that symptom almost always means a SAN mismatch, not a network problem.
Comment on lines +241 to +243

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the reviewed guide around the quoted lines.
if [ -f docs/guides/postgres/remote-replica/remotereplica.md ]; then
  echo "== file exists =="
  wc -l docs/guides/postgres/remote-replica/remotereplica.md
  sed -n '210,260p' docs/guides/postgres/remote-replica/remotereplica.md | cat -n
else
  echo "file not found"
fi

echo
echo "== nearby pg_isready / TLS / cert mentions =="
rg -n "pg_isready|SAN|certificate|TLS|certificate-authority-data|client certificate|operator logs|connection" docs/guides/postgres/remote-replica/remotereplica.md docs -g '*.md' || true

Repository: kubedb/docs

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file summary =="
wc -l docs/guides/postgres/remote-replica/remotereplica.md || true

echo
echo "== target file around quoted text =="
sed -n '220,250p' docs/guides/postgres/remote-replica/remotereplica.md | cat -n

echo
echo "== postgres remote-replica references to TLS/cert/pg_isready in target doc =="
rg -n "TLS|certificate|pg_isready|certificate-authority-data|ca.crt|replica|operator logs|logs" docs/guides/postgres/remote-replica/remotereplica.md || true

echo
echo "== focused source docs around postgres pg_isready troubleshooting =="
rg -n -C 3 "Attempting pg_isready on primary|pg_isready on primary|SAN|certificate-authority-data|TLS" docs/guides/postgres -g '*.md' || true

Repository: kubedb/docs

Length of output: 50369


Don’t rule out other TLS failures around this symptom.

A successful non-TLS pg_isready only shows network reachability. Wrong CA data, missing client certificate/key, or TLS policy errors can also leave the replica stuck retrying Attempting pg_isready on primary; add a note to check TLS configuration and KubeDB/PostgreSQL logs instead of saying it “almost always means a SAN mismatch”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guides/postgres/remote-replica/remotereplica.md` around lines 241 - 243,
Update the TLS troubleshooting guidance around “Attempting pg_isready on
primary” so it does not imply SAN mismatch is the primary explanation. State
that successful non-TLS connectivity only confirms network reachability, and
direct readers to verify CA data, client certificate/key, TLS policy, and
KubeDB/PostgreSQL logs for other TLS failures.


To use `verify-full`, add the external hostname to the source's server certificate. On an
existing database do it with a [ReconfigureTLS ops request](/docs/guides/postgres/reconfigure-tls/reconfigure-tls.md)
— certificates are reissued and rotated without manual restarts:

```yaml
apiVersion: ops.kubedb.com/v1alpha1
kind: PostgresOpsRequest
metadata:
name: add-external-san
namespace: demo
spec:
type: ReconfigureTLS
databaseRef:
name: pg-singapore
tls:
certificates:
- alias: server
dnsNames:
- pg-singapore.example.com # the address the replica dials
apply: Always
Comment on lines +261 to +264

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the same endpoint in -d and the certificate SAN.

Line 207 dials 172.104.37.147, but this request adds only the DNS SAN pg-singapore.example.com. verify-full checks the endpoint used by the connection, so the documented IP command still fails TLS verification. Use pg-singapore.example.com:5432 in -d, or document a certificate request that adds the dialed IP as an IP SAN.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guides/postgres/remote-replica/remotereplica.md` around lines 261 - 264,
Update the remote replica documentation so the endpoint used by the connection
command matches the certificate SAN: change the `-d` example to dial
`pg-singapore.example.com:5432`, or add the dialed IP as an IP SAN in the
certificate request. Preserve the existing `server` alias and `dnsNames`
configuration.

```

Alternatively, edit the generated AppBinding's `spec.clientConfig.service.query` to
`sslmode=verify-ca`.

# Create Remote Replica
We have prepared another cluster in london region for replicating across cluster. follow the installation instruction [above](/docs/README.md).

Expand Down Expand Up @@ -285,6 +347,21 @@ NAME VERSION STATUS AGE
pg-london 18.3 Ready 7m17s
```

Each remote replica pod runs `2/2` containers: `postgres` plus a `pg-coordinator` sidecar
in remote-replica mode (no Raft). The coordinator monitors streaming against the source,
recovers the replica with `pg_rewind` or a fresh basebackup when the source changes
timeline, keeps the pod's `standby` role label truthful (set only while the source
confirms the pod is streaming, cleared otherwise), and logs the replication lag:

```bash
$ kubectl logs pg-london-0 -n demo -c pg-coordinator | grep LagMonitor
[LagMonitor] Pod pg-london-0: lag=0 B (in sync with source); next check in 40s
```

A `<name>-standby` Service is created for remote replicas at any replica count and selects
the pods whose standby label is set — use it (not the primary-selecting `<name>` Service,
which has no endpoints while the database is a replica) to route read-only traffic.

## Validate Remote Replica

At this point we want to validate the replication, we can see `pg-london-0` is connected as asynchronous replica
Expand Down
Loading