E-mail observability: van logregels naar volledige zichtbaarheid
Hoe je structured logging, distributed tracing en slimme alerting inzet om elk e-mailprobleem binnen minuten te vinden.
Het probleem: "De klant heeft mijn e-mail niet ontvangen"
Elke ontwikkelaar die ooit met e-mail heeft gewerkt, kent dit moment. Een collega, klant of manager komt naar je toe: *"Ik heb geen bevestigingsmail ontvangen."* En dan begint de speurtocht. Je zoekt in serverlogboeken, controleert SMTP-responses, vraagt of ze hun spamfolder hebben gecheckt — en na een uur weet je nog steeds niet precies wat er is gebeurd.
Dit is geen edge case. E-mail is inherent onbetrouwbaar en asynchroon. Tussen het moment dat je applicatie sendMail() aanroept en het moment dat de ontvanger de mail in zijn inbox ziet, passeert het bericht gemiddeld 4 tot 7 systemen. Elk van die systemen kan vertragen, weigeren of stilzwijgend falen.
De oplossing? E-mail observability — een systematische aanpak om elk punt in je e-mailpipeline zichtbaar, doorzoekbaar en alerteerbaar te maken.
---
Wat is observability (en waarom verschilt het van monitoring)?
Monitoring vertelt je *dat* er iets fout gaat. Observability vertelt je *waarom*.
| Aspect | Monitoring | Observability |
|---|---|---|
| Vraag | "Werkt het?" | "Waarom werkt het niet?" |
| Data | Metrics, uptime checks | Logs, traces, metrics, events |
| Aanpak | Vooraf gedefinieerde dashboards | Ad-hoc queries op ruwe data |
| Voorbeeld | "Bounce rate is 8%" | "Bounces komen van IP 198.51.x bij Gmail, sinds 14:23, na een DNS-wijziging" |
In de context van e-mail heb je drie pijlers nodig:
- Structured logging — Machine-leesbare logregels met context
- Distributed tracing — Eén trace per e-mail, van applicatie tot aflevering
- Metrics & alerting — Realtime dashboards met slimme drempels
---
Pijler 1: Structured logging voor e-mail
Het probleem met traditionele logs
De meeste MTA's (Postfix, Sendmail, Exim) loggen naar syslog in een ongestructureerd tekstformaat:
Mar 10 05:23:17 mail01 postfix/smtp[12345]: 3F2A41234: to=<klant@example.com>, relay=mx.example.com[93.184.216.34]:25, delay=1.2, delays=0.1/0.01/0.8/0.3, dsn=2.0.0, status=sent (250 OK)Dit is *leesbaar* voor een mens, maar probeer hier eens op te aggregeren. Hoeveel mails naar Gmail zijn de afgelopen uur gefaald? Wat is de gemiddelde delay per relay? Je hebt regex-parsing nodig, en dat schaalt niet.
De oplossing: JSON-gestructureerde logs
json{ "timestamp": "2026-03-10T05:23:17.445Z", "level": "info", "service": "mta", "event": "email.delivered", "messageId": "<abc123@mailbelly.com>", "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "from": "noreply@klant.nl", "to": "ontvanger@example.com", "domain": "example.com", "relay": "mx.example.com", "relayIp": "93.184.216.34", "dsn": "2.0.0", "status": "sent", "delayTotal": 1.2, "delayQueue": 0.1, "delayConnect": 0.8, "delayTransfer": 0.3, "size": 15234, "encrypted": true, "tlsVersion": "TLSv1.3" }
Voordelen:
- Direct querybaar in Elasticsearch, Loki, ClickHouse of BigQuery
- Elk veld is filterbaar en aggregeerbaar
- Geen regex-parsing, geen fragiele extractiescripts
- Correleerbaar via
traceIdenmessageId
Welke events log je?
Een volledige e-mail observability pipeline logt minimaal deze events:
Contextpropagatie: de sleutel
Het allerbelangrijkste: geef elke e-mail een unieke trace-ID mee bij creatie en propageer die door je hele stack. Van je applicatiecode, via je API, door de queue, naar de MTA, en terug via webhooks en feedback loops.
typescript// Bij het aanmaken van een e-mail const traceId = crypto.randomUUID().replace(/-/g, ''); const messageId = \`<\${traceId}@mailbelly.com>\`; await emailQueue.add({ traceId, messageId, to: recipient, template: 'welcome', // ... }); logger.info({ event: 'email.created', traceId, messageId, to: recipient, template: 'welcome', });
---
Pijler 2: Distributed tracing voor e-mail
Waarom traces?
Logs vertellen je wat er op één plek is gebeurd. Maar e-mail passeert meerdere systemen:
App → API Gateway → Email Service → Queue (Redis/SQS) → MTA → Remote MX
↓
Bounce processor ← DSN
↓
Webhook → Klant callbackMet distributed tracing (OpenTelemetry) kun je één enkel bericht volgen door al deze systemen:
In dit voorbeeld zie je direct dat de remote server 350ms nodig had om te verwerken en de TLS-handshake 120ms duurde. Zonder tracing zou je alleen weten: "het duurde 777ms" — maar niet *waar* de tijd zat.
OpenTelemetry implementatie
typescriptimport { trace, SpanStatusCode } from '@opentelemetry/api'; const tracer = trace.getTracer('mailbelly-email'); async function sendEmail(params: EmailParams) { return tracer.startActiveSpan('email.send', async (span) => { span.setAttributes({ 'email.to': params.to, 'email.from': params.from, 'email.template': params.template, 'email.domain': params.to.split('@')[1], }); try { // Queue fase const queueSpan = tracer.startSpan('email.queue.enqueue'); await queue.add(params); queueSpan.end(); // MTA fase (asynchroon — wordt later gekoppeld via traceId) span.addEvent('email.queued', { 'queue.position': await queue.length(), }); span.setStatus({ code: SpanStatusCode.OK }); } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: error.message, }); span.recordException(error); throw error; } finally { span.end(); } }); }
Het asynchrone probleem
E-mail is inherent asynchroon. De SMTP-aflevering gebeurt minuten of uren na de API-call. Hoe koppel je de trace?
Oplossing: Trace linking
typescript// In de MTA worker, wanneer de e-mail wordt afgeleverd const parentContext = trace.setSpanContext( context.active(), { traceId: email.traceId, // Originele trace-ID spanId: crypto.randomBytes(8).toString('hex'), traceFlags: 1, } ); tracer.startActiveSpan('email.smtp.deliver', { links: [{ context: parentContext }] }, async (span) => { // SMTP-aflevering });
Hiermee kun je in Jaeger, Tempo of Honeycomb de volledige reis van een e-mail volgen — ook als de aflevering pas uren later plaatsvindt.
---
Pijler 3: Metrics en alerting
De metrics die er echt toe doen
Niet alle metrics zijn gelijk. Focus op deze kernmetrics:
Slimme alerting: vermijd alert fatigue
Het grootste probleem met monitoring is alert fatigue — te veel alerts waardoor je de belangrijke mist. Gebruik deze strategie:
1. Gelaagde alerts
yaml# Voorbeeld Prometheus alerting rules groups: - name: email_alerts rules: # P1: Onmiddellijk actie vereist - alert: EmailDeliveryRateCritical expr: rate(email_delivered_total[5m]) / rate(email_sent_total[5m]) < 0.85 for: 5m labels: severity: critical annotations: summary: "Delivery rate onder 85% — mogelijke blacklisting" # P2: Onderzoek binnen 1 uur - alert: EmailQueueBacklog expr: email_queue_depth > 10000 for: 15m labels: severity: warning annotations: summary: "Queue bevat > 10k berichten" # P3: Informationeel, check bij volgende shift - alert: EmailTLSFailureElevated expr: rate(email_tls_failures_total[1h]) / rate(email_connections_total[1h]) > 0.02 for: 30m labels: severity: info
2. Anomaliedetectie in plaats van statische drempels
Statische drempels werken slecht voor e-mail. Je verzendt 's nachts minder dan overdag, op maandag meer dan op zondag. Gebruik seizoenscorrectie:
# Vergelijk huidige rate met dezelfde tijd vorige week
email_delivery_rate < 0.9 * avg_over_time(email_delivery_rate[1w] offset 1w)3. Per-domein alerting
Eén grote klant die naar Gmail stuurt, kan je overall metrics maskeren. Alert per ontvangstdomein:
# Per-domein bounce rate
rate(email_bounced_total{bounce_type="hard"}[1h])
/ rate(email_sent_total[1h])
> 0.05
# Gegroepeerd op: recipient_domain---
De e-mail observability stack
Aanbevolen architectuur
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Applicatie │────▶│ Email API │────▶│ Message Queue │
│ (traces + │ │ (traces + │ │ (metrics) │
│ logs) │ │ logs) │ │ │
└─────────────┘ └──────────────┘ └────────┬────────┘
│
┌────────▼────────┐
│ MTA │
│ (traces + │
│ logs + metrics)│
└────────┬────────┘
│
┌──────────────┐ ┌─────────▼────────┐
│ Feedback │◀────│ Remote MX │
│ Processor │ │ (DSN/bounces) │
│ (logs) │ └──────────────────┘
└──────┬───────┘
│
┌────────────▼────────────────┐
│ Observability Backend │
│ ┌───────┐ ┌──────┐ ┌─────┐ │
│ │ Loki │ │Tempo │ │Prom │ │
│ │(logs) │ │(trc) │ │(met)│ │
│ └───┬───┘ └──┬───┘ └──┬──┘ │
│ └────────┼────────┘ │
│ Grafana │
└─────────────────────────────┘Toolkeuze
- Logs: Grafana Loki (kostenefficiënt), Elasticsearch (krachtig maar duurder), ClickHouse (snelle analytische queries)
- Traces: Grafana Tempo, Jaeger, Honeycomb
- Metrics: Prometheus + Grafana, Datadog, InfluxDB
- All-in-one SaaS: Datadog, New Relic, Honeycomb
Kosten beheersen
E-mail genereert veel logdata. Bij 100.000 mails per dag genereer je makkelijk 10+ events per mail = 1 miljoen logregels per dag. Tips:
- Sample traces in productie (bijv. 10%), maar log *alle* fouten en bounces op 100%
- Gebruik log levels — debug-logs alleen in development
- Bewaar kort — 7 dagen volledige logs, 30 dagen geaggregeerde metrics, 90 dagen alleen error-logs
- Comprimeer en tier — oude logs naar cold storage (S3/GCS)
---
Praktisch voorbeeld: een bounceprobleem debuggen
Laten we een realistisch scenario doorlopen. Je ontvangt een alert:
⚠️ Hard bounce rate voor gmail.com is 12% (drempel: 3%)
Stap 1: Scope bepalen (metrics)
Open Grafana. Filter op recipient_domain=gmail.com. Je ziet dat de bounce rate om 03:17 is gestegen van 2% naar 12%.
Stap 2: Oorzaak zoeken (logs)
Query in Loki:
{service="mta"} | json | event="email.bounced" | domain="gmail.com" | line_format "{{.dsn}} {{.bounceReason}}"Resultaat: 89% van de bounces heeft DSN 5.7.26 met reden: *"This mail has been blocked because the sender is not authenticated with SPF."*
Stap 3: Root cause (traces + logs)
Je vindt een specifieke trace. In de logs bij email.sending zie je dat het verzendende IP is gewijzigd naar 198.51.100.42 — een IP dat niet in het SPF-record staat.
Stap 4: Oplossing
bash# Controleer huidig SPF-record dig TXT _spf.klantdomein.nl # Voeg het nieuwe IP toe # v=spf1 ip4:198.51.100.42 include:_spf.mailbelly.com ~all
Totale debug-tijd: 8 minuten. Zonder observability had dit uren geduurd.
---
Checklist: e-mail observability in 5 stappen
- Structured logging implementeren — JSON-formaat, minimaal de 9 lifecycle events
- Trace-ID propagatie — Eén ID per e-mail, van creatie tot aflevering
- Core metrics exporteren — Delivery rate, bounce rate, queue depth, latency
- Dashboards bouwen — Per-domein, per-klant, per-template overzichten
- Alerting configureren — Gelaagd (P1/P2/P3), met anomaliedetectie
---
Conclusie
E-mail observability is geen luxe — het is een vereiste voor elke organisatie die e-mail serieus neemt. Het verschil tussen "we zoeken het uit" en "we weten precies wat er is gebeurd" is het verschil tussen uren en minuten debuggen. Tussen klanten die afhaken en klanten die vertrouwen hebben in je platform.
De drie pijlers — structured logging, distributed tracing en slimme alerting — vormen samen een systeem dat je niet alleen vertelt *dat* er iets fout gaat, maar ook *wat*, *waar*, *wanneer* en *waarom*.
Begin klein: structured logs en een basis-dashboard. Bouw van daaruit op naar tracing en anomaliedetectie. De investering verdient zich terug bij de eerste serieuze incident.
---
*MailBelly biedt ingebouwde observability: structured logs, per-domein metrics en realtime alerting. Elke e-mail is traceerbaar van API-call tot inbox — zodat je nooit meer hoeft te gokken.*