Technical10 min leestijd10 maart 2026

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*.

AspectMonitoringObservability
Vraag"Werkt het?""Waarom werkt het niet?"
DataMetrics, uptime checksLogs, traces, metrics, events
AanpakVooraf gedefinieerde dashboardsAd-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:

  1. Structured logging — Machine-leesbare logregels met context
  2. Distributed tracing — Eén trace per e-mail, van applicatie tot aflevering
  3. 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 traceId en messageId

Welke events log je?

Een volledige e-mail observability pipeline logt minimaal deze events:

type: table
title: E-mail lifecycle events
headers: [Event, Wanneer, Cruciale velden]
rows:
- [email.created, "Applicatie roept send API aan", "traceId, from, to, templateId, userId"]
- [email.queued, "Bericht in wachtrij geplaatst", "traceId, queuePosition, priority"]
- [email.sending, "MTA start SMTP-sessie", "traceId, relay, relayIp, attempt"]
- [email.delivered, "250 OK ontvangen", "traceId, dsn, delay*, tlsVersion"]
- [email.bounced, "4xx/5xx ontvangen", "traceId, dsn, bounceType, bounceReason"]
- [email.deferred, "Tijdelijke fout, retry gepland", "traceId, retryAt, attempt, reason"]
- [email.opened, "Tracking pixel geladen", "traceId, userAgent, ip (geanonimiseerd)"]
- [email.clicked, "Link in mail geklikt", "traceId, url, userAgent"]
- [email.complained, "Spam-klacht via feedback loop", "traceId, feedbackType, reporter"]

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 callback

Met distributed tracing (OpenTelemetry) kun je één enkel bericht volgen door al deze systemen:

type: bar
title: Tijdverdeling per fase (ms) — voorbeeldtrace
labels: [API ontvangst, Validatie, Queue write, Queue wait, MTA pickup, DNS lookup, TLS handshake, DATA transfer, Remote verwerking]
data:
- label: Duur (ms)
values: [12, 8, 3, 145, 5, 45, 120, 89, 350]
color: "#4f46e5"

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

typescript
import { 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:

type: table
title: Essentiële e-mail metrics
headers: [Metric, Type, Alert drempel, Waarom]
rows:
- [Delivery rate, Percentage, "< 95%", "Directe indicator van e-mailgezondheid"]
- [Bounce rate (hard), Percentage, "> 3%", "Signaleert lijstkwaliteitsproblemen"]
- [Bounce rate (soft), Percentage, "> 10%", "Signaleert infrastructuurproblemen"]
- [Queue depth, Gauge, "> 10.000", "Ophoping duidt op downstream-probleem"]
- [Queue age (p95), Histogram, "> 30 min", "Oude berichten = structureel probleem"]
- [SMTP latency (p99), Histogram, "> 5s", "Trage aflevering per domein detecteren"]
- [TLS failure rate, Percentage, "> 1%", "Certificaatproblemen vroeg opsporen"]
- [Spam complaint rate, Percentage, "> 0.1%", "Google/Yahoo eis: onder 0.3% blijven"]
- [Feedback loop reports, Counter, "> 5/uur", "Reputatieproblemen detecteren"]

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:

  1. Sample traces in productie (bijv. 10%), maar log *alle* fouten en bounces op 100%
  2. Gebruik log levels — debug-logs alleen in development
  3. Bewaar kort — 7 dagen volledige logs, 30 dagen geaggregeerde metrics, 90 dagen alleen error-logs
  4. 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

  1. Structured logging implementeren — JSON-formaat, minimaal de 9 lifecycle events
  2. Trace-ID propagatie — Eén ID per e-mail, van creatie tot aflevering
  3. Core metrics exporteren — Delivery rate, bounce rate, queue depth, latency
  4. Dashboards bouwen — Per-domein, per-klant, per-template overzichten
  5. 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.*

#observability#logging#tracing#monitoring#alerting#OpenTelemetry#SRE

Klaar om te beginnen?

Start gratis met MailBelly. Geen creditcard nodig.

Gratis account aanmaken →

Vond je dit interessant?

Ontvang nieuwe artikelen direct in je inbox.