mep du suivi des stats mail avec prométheus/grafana
This commit is contained in:
@@ -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()
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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