mep du suivi des stats mail avec prométheus/grafana

This commit is contained in:
fab
2026-09-18 01:57:06 +02:00
parent 495e9e0e21
commit 89d1f13f12
8 changed files with 323 additions and 2 deletions
+9
View File
@@ -25,6 +25,15 @@ services:
networks: networks:
- monitoringNet - 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: grafana:
image: grafana/grafana:13.2.2 image: grafana/grafana:13.2.2
restart: unless-stopped restart: unless-stopped
@@ -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"]
@@ -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()
+4 -2
View File
@@ -27,8 +27,10 @@ services:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/ssl:/etc/ssl:ro - /etc/ssl:/etc/ssl:ro
# - /etc/ssl:/etc/ssl:ro # compteur de mails
# - /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro - ./pflogsumm:/var/lib/pflogsumm
- ./pflogsumm/pflogsumm.sh:/usr/local/bin/pflogsumm.sh:ro
- ./pflogsumm/pflogsumm.cron:/etc/cron.d/pflogsumm:ro
environment: environment:
- HOSTNAME=${smtpHost} - HOSTNAME=${smtpHost}
- DOMAINNAME=${domain} - DOMAINNAME=${domain}
+1
View File
@@ -0,0 +1 @@
*/5 * * * * root /usr/local/bin/pflogsumm.sh
+9
View File
@@ -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