From 89d1f13f121f7bfbda7f36706ccc3d41e1167531 Mon Sep 17 00:00:00 2001 From: Fab Date: Fri, 18 Sep 2026 01:57:06 +0200 Subject: [PATCH] =?UTF-8?q?mep=20du=20suivi=20des=20stats=20mail=20avec=20?= =?UTF-8?q?prom=C3=A9theus/grafana?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dockers/grafana/docker-compose.yml | 9 + dockers/grafana/pflogsumm-exporter/Dockerfile | 11 + .../pflogsumm-exporter/pflogsumm-exporter.py | 289 ++++++++++++++++++ dockers/postfix/docker-compose.yml | 6 +- .../postfix/pflogsumm/pflogsumm-state.json | 0 dockers/postfix/pflogsumm/pflogsumm.cron | 1 + dockers/postfix/pflogsumm/pflogsumm.sh | 9 + dockers/postfix/pflogsumm/pflogsumm.txt | 0 8 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 dockers/grafana/pflogsumm-exporter/Dockerfile create mode 100644 dockers/grafana/pflogsumm-exporter/pflogsumm-exporter.py create mode 100644 dockers/postfix/pflogsumm/pflogsumm-state.json create mode 100644 dockers/postfix/pflogsumm/pflogsumm.cron create mode 100755 dockers/postfix/pflogsumm/pflogsumm.sh create mode 100644 dockers/postfix/pflogsumm/pflogsumm.txt diff --git a/dockers/grafana/docker-compose.yml b/dockers/grafana/docker-compose.yml index d29d04b..ac6f2bc 100644 --- a/dockers/grafana/docker-compose.yml +++ b/dockers/grafana/docker-compose.yml @@ -25,6 +25,15 @@ services: networks: - monitoringNet + pflogsumm-exporter: + build: ./pflogsumm-exporter + restart: unless-stopped + container_name: pflogsumm-exporter + volumes: + - /kaz/dockers/postfix/pflogsumm:/var/lib/pflogsumm:rw + networks: + - monitoringNet + grafana: image: grafana/grafana:13.2.2 restart: unless-stopped diff --git a/dockers/grafana/pflogsumm-exporter/Dockerfile b/dockers/grafana/pflogsumm-exporter/Dockerfile new file mode 100644 index 0000000..7e68924 --- /dev/null +++ b/dockers/grafana/pflogsumm-exporter/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.13-slim + +RUN pip install --no-cache-dir prometheus-client + +WORKDIR /app + +COPY pflogsumm-exporter.py . + +EXPOSE 9912 + +CMD ["python3", "/app/pflogsumm-exporter.py"] diff --git a/dockers/grafana/pflogsumm-exporter/pflogsumm-exporter.py b/dockers/grafana/pflogsumm-exporter/pflogsumm-exporter.py new file mode 100644 index 0000000..a98c016 --- /dev/null +++ b/dockers/grafana/pflogsumm-exporter/pflogsumm-exporter.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 + +import json +import re +from http.server import BaseHTTPRequestHandler, HTTPServer + +from prometheus_client import CollectorRegistry, Gauge, generate_latest + + +REPORT_FILE = "/var/lib/pflogsumm/pflogsumm.txt" +STATE_FILE = "/var/lib/pflogsumm/pflogsumm-state.json" +PORT = 9912 + + +METRIC_NAMES = ( + "received", + "delivered", + "deferred", + "bounced", + "rejected", +) + + +def parse_report(): + metrics = { + "received": 0, + "delivered": 0, + "deferred": 0, + "bounced": 0, + "rejected": 0, + } + + hourly = [] + + try: + with open(REPORT_FILE, "r", encoding="utf-8") as f: + content = f.read() + except Exception: + return metrics, hourly + + # Grand Totals + patterns = { + "received": r"^\s*(\d+)\s+received\s*$", + "delivered": r"^\s*(\d+)\s+delivered\s*$", + "deferred": r"^\s*(\d+)\s+deferred", + "bounced": r"^\s*(\d+)\s+bounced\s*$", + "rejected": r"^\s*(\d+)\s+rejected", + } + + for name, pattern in patterns.items(): + match = re.search(pattern, content, re.MULTILINE) + if match: + metrics[name] = int(match.group(1)) + + # Per-Hour Traffic Summary + in_hourly = False + + for line in content.splitlines(): + if line.startswith("Per-Hour Traffic Summary"): + in_hourly = True + continue + + if not in_hourly: + continue + + match = re.match( + r"^\s*(\d{4})-(\d{4})\s+" + r"(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)", + line, + ) + + if match: + start, end, received, delivered, deferred, bounced, rejected = match.groups() + + hourly.append({ + "hour": start[:2], + "received": int(received), + "delivered": int(delivered), + "deferred": int(deferred), + "bounced": int(bounced), + "rejected": int(rejected), + }) + + return metrics, hourly + + +def load_state(): + try: + with open(STATE_FILE, "r", encoding="utf-8") as f: + state = json.load(f) + + if "last" not in state or "total" not in state: + raise ValueError("invalid state") + + return state + + except Exception: + return { + "last": { + name: 0 + for name in METRIC_NAMES + }, + "total": { + name: 0 + for name in METRIC_NAMES + }, + } + + +def save_state(state): + tmp_file = STATE_FILE + ".tmp" + + with open(tmp_file, "w", encoding="utf-8") as f: + json.dump(state, f) + + # Remplacement atomique du fichier d'état. + import os + os.replace(tmp_file, STATE_FILE) + + +def update_totals(current): + state = load_state() + + for name in METRIC_NAMES: + previous = state["last"][name] + value = current[name] + + if value >= previous: + # Même journée : on ajoute uniquement ce qui est nouveau. + state["total"][name] += value - previous + else: + # Le compteur pflogsumm a diminué : + # on considère qu'un nouveau jour a commencé. + state["total"][name] += value + + state["last"][name] = value + + save_state(state) + + return state["total"] + + +def generate_metrics(): + registry = CollectorRegistry() + + received = Gauge( + "postfix_pflogsumm_messages_received", + "Number of messages received today", + registry=registry, + ) + delivered = Gauge( + "postfix_pflogsumm_messages_delivered", + "Number of deliveries today", + registry=registry, + ) + deferred = Gauge( + "postfix_pflogsumm_messages_deferred", + "Number of deferred messages today", + registry=registry, + ) + bounced = Gauge( + "postfix_pflogsumm_messages_bounced", + "Number of bounced messages today", + registry=registry, + ) + rejected = Gauge( + "postfix_pflogsumm_messages_rejected", + "Number of rejected messages today", + registry=registry, + ) + + received_total = Gauge( + "postfix_pflogsumm_messages_received_total", + "Cumulative number of messages received", + registry=registry, + ) + delivered_total = Gauge( + "postfix_pflogsumm_messages_delivered_total", + "Cumulative number of deliveries", + registry=registry, + ) + deferred_total = Gauge( + "postfix_pflogsumm_messages_deferred_total", + "Cumulative number of deferred messages", + registry=registry, + ) + bounced_total = Gauge( + "postfix_pflogsumm_messages_bounced_total", + "Cumulative number of bounced messages", + registry=registry, + ) + rejected_total = Gauge( + "postfix_pflogsumm_messages_rejected_total", + "Cumulative number of rejected messages", + registry=registry, + ) + + hourly_received = Gauge( + "postfix_pflogsumm_messages_received_hourly", + "Messages received during the hour", + ["hour"], + registry=registry, + ) + hourly_delivered = Gauge( + "postfix_pflogsumm_messages_delivered_hourly", + "Deliveries during the hour", + ["hour"], + registry=registry, + ) + hourly_deferred = Gauge( + "postfix_pflogsumm_messages_deferred_hourly", + "Deferred messages during the hour", + ["hour"], + registry=registry, + ) + hourly_bounced = Gauge( + "postfix_pflogsumm_messages_bounced_hourly", + "Bounced messages during the hour", + ["hour"], + registry=registry, + ) + hourly_rejected = Gauge( + "postfix_pflogsumm_messages_rejected_hourly", + "Rejected messages during the hour", + ["hour"], + registry=registry, + ) + + totals, hourly = parse_report() + + received.set(totals["received"]) + delivered.set(totals["delivered"]) + deferred.set(totals["deferred"]) + bounced.set(totals["bounced"]) + rejected.set(totals["rejected"]) + + persistent = update_totals(totals) + + received_total.set(persistent["received"]) + delivered_total.set(persistent["delivered"]) + deferred_total.set(persistent["deferred"]) + bounced_total.set(persistent["bounced"]) + rejected_total.set(persistent["rejected"]) + + for row in hourly: + labels = {"hour": row["hour"]} + + hourly_received.labels(**labels).set(row["received"]) + hourly_delivered.labels(**labels).set(row["delivered"]) + hourly_deferred.labels(**labels).set(row["deferred"]) + hourly_bounced.labels(**labels).set(row["bounced"]) + hourly_rejected.labels(**labels).set(row["rejected"]) + + return generate_latest(registry) + + +class MetricsHandler(BaseHTTPRequestHandler): + + def do_GET(self): + if self.path != "/metrics": + self.send_response(404) + self.end_headers() + return + + try: + data = generate_metrics() + + self.send_response(200) + self.send_header( + "Content-Type", + "text/plain; version=0.0.4; charset=utf-8", + ) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + except Exception as e: + self.send_response(500) + self.end_headers() + self.wfile.write(str(e).encode()) + + def log_message(self, format, *args): + return + + +if __name__ == "__main__": + server = HTTPServer(("0.0.0.0", PORT), MetricsHandler) + print(f"pflogsumm-exporter listening on port {PORT}", flush=True) + server.serve_forever() diff --git a/dockers/postfix/docker-compose.yml b/dockers/postfix/docker-compose.yml index 6b2533a..c285f57 100644 --- a/dockers/postfix/docker-compose.yml +++ b/dockers/postfix/docker-compose.yml @@ -27,8 +27,10 @@ services: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /etc/ssl:/etc/ssl:ro -# - /etc/ssl:/etc/ssl:ro -# - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro + # compteur de mails + - ./pflogsumm:/var/lib/pflogsumm + - ./pflogsumm/pflogsumm.sh:/usr/local/bin/pflogsumm.sh:ro + - ./pflogsumm/pflogsumm.cron:/etc/cron.d/pflogsumm:ro environment: - HOSTNAME=${smtpHost} - DOMAINNAME=${domain} diff --git a/dockers/postfix/pflogsumm/pflogsumm-state.json b/dockers/postfix/pflogsumm/pflogsumm-state.json new file mode 100644 index 0000000..e69de29 diff --git a/dockers/postfix/pflogsumm/pflogsumm.cron b/dockers/postfix/pflogsumm/pflogsumm.cron new file mode 100644 index 0000000..7151e7c --- /dev/null +++ b/dockers/postfix/pflogsumm/pflogsumm.cron @@ -0,0 +1 @@ +*/5 * * * * root /usr/local/bin/pflogsumm.sh diff --git a/dockers/postfix/pflogsumm/pflogsumm.sh b/dockers/postfix/pflogsumm/pflogsumm.sh new file mode 100755 index 0000000..aae85f5 --- /dev/null +++ b/dockers/postfix/pflogsumm/pflogsumm.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +#pflogsumm -d today /var/log/mail/mail.log > /var/lib/pflogsumm/pflogsumm.txt + +/usr/sbin/pflogsumm -d today /var/log/mail/mail.log > /var/lib/pflogsumm/pflogsumm.txt.tmp + +if [ -s /var/lib/pflogsumm/pflogsumm.txt.tmp ]; then + mv /var/lib/pflogsumm/pflogsumm.txt.tmp /var/lib/pflogsumm/pflogsumm.txt +fi diff --git a/dockers/postfix/pflogsumm/pflogsumm.txt b/dockers/postfix/pflogsumm/pflogsumm.txt new file mode 100644 index 0000000..e69de29