Files
upstream-zulip/puppet/zulip/files/postgresql/wal-g-exporter
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

163 lines
6.2 KiB
Python
Executable File

#!/usr/bin/env python3
import configparser
import contextlib
import json
import logging
import os
import subprocess
import sys
from collections import defaultdict
from collections.abc import Mapping
from datetime import datetime, timedelta, timezone
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Protocol
from urllib.parse import parse_qs, urlsplit
class GaugeMetric(Protocol):
def __call__(self, value: float, labels: Mapping[str, str] | None = None, /) -> None:
pass
class WalGPrometheusServer(BaseHTTPRequestHandler):
METRIC_PREFIX = "wal_g_backup_"
metrics: dict[str, list[str]] = {}
metric_values: dict[str, dict[str, str]] = defaultdict(dict)
server_version = "wal-g-prometheus-server/1.0"
def gauge(
self, name: str, description: str | None = None, default_value: float | None = None
) -> GaugeMetric:
if name in self.metrics:
raise ValueError(f"Redefinition of {name} metric")
self.metrics[name] = [f"# TYPE {self.METRIC_PREFIX}{name} gauge"]
if description is not None:
self.metrics[name].append(f"# HELP {self.METRIC_PREFIX}{name} {description}")
def inner(value: float, labels: Mapping[str, str] | None = None) -> None:
label_str = ""
if labels:
label_str = "{" + ",".join(f'{k}="{v}"' for k, v in labels.items()) + "}"
self.metric_values[name][label_str] = f"{self.METRIC_PREFIX}{name}{label_str} {value}"
if default_value is not None:
inner(default_value)
return inner
def print_metrics(self) -> None:
lines = []
for metric_name in self.metrics:
if self.metric_values[metric_name]:
# Print preamble
lines += self.metrics[metric_name]
lines += self.metric_values[metric_name].values()
lines.append("")
self.wfile.write("\n".join(lines).encode())
def do_GET(self) -> None:
if urlsplit(self.path).path != "/metrics":
self.send_response(404)
self.end_headers()
sys.stderr.flush()
return
self.send_response(200)
self.send_header("Content-type", "text/plain; version=0.0.4")
self.end_headers()
self.metrics = {}
self.metric_values = defaultdict(dict)
backup_ok = self.gauge("ok", "If wal-g backup-list was OK", 0)
backup_count = self.gauge("count", "Number of backups")
backup_earliest_age_seconds = self.gauge("earliest_age_seconds", "Age of the oldest backup")
backup_latest_age_seconds = self.gauge(
"latest_age_seconds", "Age of the most recent backup"
)
backup_latest_duration_seconds = self.gauge(
"latest_duration_seconds", "Duration the most recent backup took, in seconds"
)
backup_latest_compressed_size_bytes = self.gauge(
"latest_compressed_size_bytes", "Size of the most recent backup, in bytes"
)
backup_latest_uncompressed_size_bytes = self.gauge(
"latest_uncompressed_size_bytes",
"Uncompressed size of the most recent backup, in bytes",
)
backup_total_compressed_size_bytes = self.gauge(
"total_compressed_size_bytes", "Total compressed size of all backups, in bytes"
)
now = datetime.now(tz=timezone.utc)
try:
query = parse_qs(urlsplit(self.path).query)
modified_env = os.environ.copy()
if query.get("bucket") and len(query["bucket"]) == 1:
bucket = query["bucket"][0]
modified_env["WALG_S3_PREFIX"] = f"s3://{bucket}"
else:
config_file = configparser.RawConfigParser()
config_file.read("/etc/zulip/zulip-secrets.conf")
bucket = config_file["secrets"]["s3_backups_bucket"]
backup_list_output = subprocess.check_output(
["env-wal-g", "backup-list", "--detail", "--json"],
text=True,
env=modified_env,
)
data = json.loads(backup_list_output) if backup_list_output else []
backup_count(len(data), {"bucket": bucket})
backup_total_compressed_size_bytes(
sum(e["compressed_size"] for e in data), {"bucket": bucket}
)
if len(data) > 0:
data.sort(key=lambda e: e["start_time"], reverse=True)
latest = data[0]
labels = {
"host": latest["hostname"],
"pg_version": str(latest["pg_version"]),
"bucket": bucket,
}
backup_latest_compressed_size_bytes(latest["compressed_size"], labels)
backup_latest_uncompressed_size_bytes(latest["uncompressed_size"], labels)
def t(key: str, e: dict[str, str] = latest) -> datetime:
return datetime.strptime(e[key], e["date_fmt"]).replace(tzinfo=timezone.utc)
backup_earliest_age_seconds(
(now - t("start_time", data[-1])) / timedelta(seconds=1),
{
"host": data[-1]["hostname"],
"pg_version": data[-1]["pg_version"],
"bucket": bucket,
},
)
backup_latest_age_seconds((now - t("start_time")) / timedelta(seconds=1), labels)
backup_latest_duration_seconds(
(t("finish_time") - t("start_time")) / timedelta(seconds=1), labels
)
backup_ok(1)
except Exception:
logging.exception("Error while getting backup information")
finally:
self.print_metrics()
self.log_message(
"Served in %.2f seconds",
(datetime.now(tz=timezone.utc) - now) / timedelta(seconds=1),
)
sys.stderr.flush()
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")
server = HTTPServer(("127.0.0.1", 9188), WalGPrometheusServer)
logging.info("Starting server...")
with contextlib.suppress(KeyboardInterrupt):
server.serve_forever()
server.server_close()
logging.info("Stopping server...")