#!/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()