mep du suivi des stats mail avec prométheus/grafana
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*/5 * * * * root /usr/local/bin/pflogsumm.sh
|
||||
Executable
+9
@@ -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
|
||||
Reference in New Issue
Block a user