monitoring: add Postmark bounce exporter
This commit is contained in:
parent
ec208fe0f6
commit
d132917d9e
120
scripts/monitoring_postmark_exporter.py
Normal file
120
scripts/monitoring_postmark_exporter.py
Normal file
@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import datetime as dt
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
import requests
|
||||
from prometheus_client import Gauge, Info, start_http_server
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Window:
|
||||
label: str
|
||||
days: int
|
||||
|
||||
|
||||
WINDOWS = [
|
||||
Window("1d", 1),
|
||||
Window("7d", 7),
|
||||
]
|
||||
|
||||
API_BASE = os.environ.get("POSTMARK_API_BASE", "https://api.postmarkapp.com").rstrip("/")
|
||||
POLL_INTERVAL_SECONDS = int(os.environ.get("POLL_INTERVAL_SECONDS", "60"))
|
||||
LISTEN_ADDRESS = os.environ.get("LISTEN_ADDRESS", "0.0.0.0")
|
||||
LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "8000"))
|
||||
|
||||
PRIMARY_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN", "").strip()
|
||||
FALLBACK_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN_FALLBACK", "").strip()
|
||||
|
||||
EXPORTER_INFO = Info("postmark_exporter", "Exporter build info")
|
||||
EXPORTER_INFO.info(
|
||||
{
|
||||
"api_base": API_BASE,
|
||||
"windows": ",".join(window.label for window in WINDOWS),
|
||||
}
|
||||
)
|
||||
|
||||
POSTMARK_API_UP = Gauge("postmark_api_up", "Whether Postmark API is reachable (1) or not (0)")
|
||||
POSTMARK_LAST_SUCCESS = Gauge(
|
||||
"postmark_last_success_timestamp_seconds",
|
||||
"Unix timestamp of the last successful Postmark stats refresh",
|
||||
)
|
||||
POSTMARK_REQUEST_ERRORS = Gauge(
|
||||
"postmark_request_errors_total",
|
||||
"Total Postmark stats request errors since exporter start",
|
||||
)
|
||||
|
||||
POSTMARK_OUTBOUND_SENT = Gauge(
|
||||
"postmark_outbound_sent",
|
||||
"Outbound emails sent within the selected window",
|
||||
labelnames=("window",),
|
||||
)
|
||||
POSTMARK_OUTBOUND_BOUNCED = Gauge(
|
||||
"postmark_outbound_bounced",
|
||||
"Outbound emails bounced within the selected window",
|
||||
labelnames=("window",),
|
||||
)
|
||||
POSTMARK_OUTBOUND_BOUNCE_RATE = Gauge(
|
||||
"postmark_outbound_bounce_rate",
|
||||
"Outbound bounce rate percentage within the selected window",
|
||||
labelnames=("window",),
|
||||
)
|
||||
|
||||
|
||||
def fetch_outbound_stats(token: str, window: Window) -> dict:
|
||||
today = dt.date.today()
|
||||
fromdate = today - dt.timedelta(days=window.days)
|
||||
params = {"fromdate": fromdate.isoformat(), "todate": today.isoformat()}
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"X-Postmark-Server-Token": token,
|
||||
}
|
||||
response = requests.get(
|
||||
f"{API_BASE}/stats/outbound",
|
||||
headers=headers,
|
||||
params=params,
|
||||
timeout=15,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def update_metrics(token: str) -> None:
|
||||
for window in WINDOWS:
|
||||
data = fetch_outbound_stats(token, window)
|
||||
sent = int(data.get("Sent", 0) or 0)
|
||||
bounced = int(data.get("Bounced", 0) or 0)
|
||||
rate = (bounced / sent * 100.0) if sent else 0.0
|
||||
POSTMARK_OUTBOUND_SENT.labels(window=window.label).set(sent)
|
||||
POSTMARK_OUTBOUND_BOUNCED.labels(window=window.label).set(bounced)
|
||||
POSTMARK_OUTBOUND_BOUNCE_RATE.labels(window=window.label).set(rate)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not PRIMARY_TOKEN and not FALLBACK_TOKEN:
|
||||
raise SystemExit("POSTMARK_SERVER_TOKEN or POSTMARK_SERVER_TOKEN_FALLBACK is required")
|
||||
|
||||
start_http_server(LISTEN_PORT, addr=LISTEN_ADDRESS)
|
||||
|
||||
tokens = [token for token in (PRIMARY_TOKEN, FALLBACK_TOKEN) if token]
|
||||
token_index = 0
|
||||
|
||||
while True:
|
||||
token = tokens[token_index % len(tokens)]
|
||||
token_index += 1
|
||||
try:
|
||||
update_metrics(token)
|
||||
POSTMARK_API_UP.set(1)
|
||||
POSTMARK_LAST_SUCCESS.set(time.time())
|
||||
except Exception as exc: # noqa: BLE001
|
||||
POSTMARK_API_UP.set(0)
|
||||
POSTMARK_REQUEST_ERRORS.inc()
|
||||
print(f"postmark_exporter: refresh failed: {exc}", flush=True)
|
||||
time.sleep(POLL_INTERVAL_SECONDS)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
35
scripts/monitoring_render_postmark_exporter.py
Normal file
35
scripts/monitoring_render_postmark_exporter.py
Normal file
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def indent(text: str, spaces: int) -> str:
|
||||
prefix = " " * spaces
|
||||
return "".join(prefix + line if line.strip("\n") else line for line in text.splitlines(keepends=True))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = root / "scripts" / "monitoring_postmark_exporter.py"
|
||||
target = root / "services" / "monitoring" / "postmark-exporter-script.yaml"
|
||||
|
||||
payload = source.read_text(encoding="utf-8")
|
||||
if not payload.endswith("\n"):
|
||||
payload += "\n"
|
||||
|
||||
yaml = (
|
||||
f"# services/monitoring/postmark-exporter-script.yaml\n"
|
||||
f"apiVersion: v1\n"
|
||||
f"kind: ConfigMap\n"
|
||||
f"metadata:\n"
|
||||
f" name: postmark-exporter-script\n"
|
||||
f"data:\n"
|
||||
f" monitoring_postmark_exporter.py: |\n"
|
||||
f"{indent(payload, 4)}"
|
||||
)
|
||||
|
||||
target.write_text(yaml, encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -5,6 +5,7 @@ namespace: monitoring
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- rbac.yaml
|
||||
- postmark-exporter-script.yaml
|
||||
- grafana-dashboard-overview.yaml
|
||||
- grafana-dashboard-pods.yaml
|
||||
- grafana-dashboard-nodes.yaml
|
||||
@ -12,6 +13,8 @@ resources:
|
||||
- grafana-dashboard-network.yaml
|
||||
- grafana-dashboard-gpu.yaml
|
||||
- dcgm-exporter.yaml
|
||||
- postmark-exporter-service.yaml
|
||||
- postmark-exporter-deployment.yaml
|
||||
- grafana-folders.yaml
|
||||
- helmrelease.yaml
|
||||
- grafana-org-bootstrap.yaml
|
||||
|
||||
63
services/monitoring/postmark-exporter-deployment.yaml
Normal file
63
services/monitoring/postmark-exporter-deployment.yaml
Normal file
@ -0,0 +1,63 @@
|
||||
# services/monitoring/postmark-exporter-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postmark-exporter
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postmark-exporter
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postmark-exporter
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8000"
|
||||
prometheus.io/path: "/metrics"
|
||||
spec:
|
||||
containers:
|
||||
- name: exporter
|
||||
image: python:3.12-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -euo pipefail
|
||||
pip install --no-cache-dir prometheus-client==0.22.1 requests==2.32.3
|
||||
exec python /app/monitoring_postmark_exporter.py
|
||||
env:
|
||||
- name: POSTMARK_SERVER_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postmark-exporter
|
||||
key: relay-username
|
||||
- name: POSTMARK_SERVER_TOKEN_FALLBACK
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postmark-exporter
|
||||
key: relay-password
|
||||
- name: POLL_INTERVAL_SECONDS
|
||||
value: "60"
|
||||
- name: LISTEN_PORT
|
||||
value: "8000"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
volumeMounts:
|
||||
- name: script
|
||||
mountPath: /app
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
volumes:
|
||||
- name: script
|
||||
configMap:
|
||||
name: postmark-exporter-script
|
||||
|
||||
127
services/monitoring/postmark-exporter-script.yaml
Normal file
127
services/monitoring/postmark-exporter-script.yaml
Normal file
@ -0,0 +1,127 @@
|
||||
# services/monitoring/postmark-exporter-script.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: postmark-exporter-script
|
||||
data:
|
||||
monitoring_postmark_exporter.py: |
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import datetime as dt
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
import requests
|
||||
from prometheus_client import Gauge, Info, start_http_server
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Window:
|
||||
label: str
|
||||
days: int
|
||||
|
||||
|
||||
WINDOWS = [
|
||||
Window("1d", 1),
|
||||
Window("7d", 7),
|
||||
]
|
||||
|
||||
API_BASE = os.environ.get("POSTMARK_API_BASE", "https://api.postmarkapp.com").rstrip("/")
|
||||
POLL_INTERVAL_SECONDS = int(os.environ.get("POLL_INTERVAL_SECONDS", "60"))
|
||||
LISTEN_ADDRESS = os.environ.get("LISTEN_ADDRESS", "0.0.0.0")
|
||||
LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "8000"))
|
||||
|
||||
PRIMARY_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN", "").strip()
|
||||
FALLBACK_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN_FALLBACK", "").strip()
|
||||
|
||||
EXPORTER_INFO = Info("postmark_exporter", "Exporter build info")
|
||||
EXPORTER_INFO.info(
|
||||
{
|
||||
"api_base": API_BASE,
|
||||
"windows": ",".join(window.label for window in WINDOWS),
|
||||
}
|
||||
)
|
||||
|
||||
POSTMARK_API_UP = Gauge("postmark_api_up", "Whether Postmark API is reachable (1) or not (0)")
|
||||
POSTMARK_LAST_SUCCESS = Gauge(
|
||||
"postmark_last_success_timestamp_seconds",
|
||||
"Unix timestamp of the last successful Postmark stats refresh",
|
||||
)
|
||||
POSTMARK_REQUEST_ERRORS = Gauge(
|
||||
"postmark_request_errors_total",
|
||||
"Total Postmark stats request errors since exporter start",
|
||||
)
|
||||
|
||||
POSTMARK_OUTBOUND_SENT = Gauge(
|
||||
"postmark_outbound_sent",
|
||||
"Outbound emails sent within the selected window",
|
||||
labelnames=("window",),
|
||||
)
|
||||
POSTMARK_OUTBOUND_BOUNCED = Gauge(
|
||||
"postmark_outbound_bounced",
|
||||
"Outbound emails bounced within the selected window",
|
||||
labelnames=("window",),
|
||||
)
|
||||
POSTMARK_OUTBOUND_BOUNCE_RATE = Gauge(
|
||||
"postmark_outbound_bounce_rate",
|
||||
"Outbound bounce rate percentage within the selected window",
|
||||
labelnames=("window",),
|
||||
)
|
||||
|
||||
|
||||
def fetch_outbound_stats(token: str, window: Window) -> dict:
|
||||
today = dt.date.today()
|
||||
fromdate = today - dt.timedelta(days=window.days)
|
||||
params = {"fromdate": fromdate.isoformat(), "todate": today.isoformat()}
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"X-Postmark-Server-Token": token,
|
||||
}
|
||||
response = requests.get(
|
||||
f"{API_BASE}/stats/outbound",
|
||||
headers=headers,
|
||||
params=params,
|
||||
timeout=15,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def update_metrics(token: str) -> None:
|
||||
for window in WINDOWS:
|
||||
data = fetch_outbound_stats(token, window)
|
||||
sent = int(data.get("Sent", 0) or 0)
|
||||
bounced = int(data.get("Bounced", 0) or 0)
|
||||
rate = (bounced / sent * 100.0) if sent else 0.0
|
||||
POSTMARK_OUTBOUND_SENT.labels(window=window.label).set(sent)
|
||||
POSTMARK_OUTBOUND_BOUNCED.labels(window=window.label).set(bounced)
|
||||
POSTMARK_OUTBOUND_BOUNCE_RATE.labels(window=window.label).set(rate)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not PRIMARY_TOKEN and not FALLBACK_TOKEN:
|
||||
raise SystemExit("POSTMARK_SERVER_TOKEN or POSTMARK_SERVER_TOKEN_FALLBACK is required")
|
||||
|
||||
start_http_server(LISTEN_PORT, addr=LISTEN_ADDRESS)
|
||||
|
||||
tokens = [token for token in (PRIMARY_TOKEN, FALLBACK_TOKEN) if token]
|
||||
token_index = 0
|
||||
|
||||
while True:
|
||||
token = tokens[token_index % len(tokens)]
|
||||
token_index += 1
|
||||
try:
|
||||
update_metrics(token)
|
||||
POSTMARK_API_UP.set(1)
|
||||
POSTMARK_LAST_SUCCESS.set(time.time())
|
||||
except Exception as exc: # noqa: BLE001
|
||||
POSTMARK_API_UP.set(0)
|
||||
POSTMARK_REQUEST_ERRORS.inc()
|
||||
print(f"postmark_exporter: refresh failed: {exc}", flush=True)
|
||||
time.sleep(POLL_INTERVAL_SECONDS)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
18
services/monitoring/postmark-exporter-service.yaml
Normal file
18
services/monitoring/postmark-exporter-service.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
# services/monitoring/postmark-exporter-service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postmark-exporter
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8000"
|
||||
prometheus.io/path: "/metrics"
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: postmark-exporter
|
||||
ports:
|
||||
- name: http
|
||||
port: 8000
|
||||
targetPort: http
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user