Observability8 min read

Grafana dashboard for inbox placement

A Grafana panel for per-provider inbox rate. A DMARC alignment heatmap. A DNSBL count. Import the dashboard JSON, point it at Inbox Check, and you have a full deliverability observability stack.

Every platform team running their own observability stack ends up asking the same question about deliverability: why is the business-critical signal (inbox rate) stuck in a third-party SaaS dashboard when every other metric on the wall is in Grafana? This article fixes that. We scrape the Inbox Check API into Prometheus, expose it as a proper time series, and import a pre-built Grafana dashboard with per-provider inbox rate, DMARC alignment, DNSBL counts, and alert rules.

Architecture in one line

Inbox Check API → small Node exporter → Prometheus scrape → Grafana dashboard. Alerts via Grafana Unified Alerting → PagerDuty / Slack. No SaaS dashboard. No extra vendor.

Why Grafana beats SaaS dashboards for serious teams

SaaS dashboards are great for a team of one. They break down for teams that already have observability discipline. Three reasons Grafana wins once you have a platform team:

  • One pane of glass. Inbox rate next to API error rate next to queue depth. Correlations are obvious; in separate tabs they are invisible.
  • Alerting in the same place as everything else. Your on-call already has Grafana alerting rules for everything else. Do not bolt on a second alerting story.
  • Retention under your control. Most SaaS dashboards keep 30–90 days. Prometheus + Thanos / Mimir gives you whatever retention you want.

Architecture

Two moving parts. The first is a small exporter that calls the Inbox Check API on a schedule, keeps the last known good result per monitor, and exposes it at /metrics in Prometheus text format. The second is the Grafana dashboard that queries Prometheus for those series.

You do not strictly need Prometheus — Grafana has a JSON API datasource plugin that can hit the Inbox Check API directly. That works, but you lose time-series retention, joins with other metrics, and recording rules. For a one-off panel it is fine; for a production dashboard, use the exporter.

Two options: direct JSON or Prom exporter

Direct JSON (quick and dirty)

Install the marcusolsson-json-datasource Grafana plugin. Point it at https://check.live-direct-marketing.online/api. Add the Authorization: Bearer ic_live_... header. Query paths like /check/{id} in individual panels. You will not get time-series though — every panel shows the current value only.

Prometheus exporter (recommended)

A 60-line Node process that does two jobs: (1) runs a setInterval every N minutes to kick off new placement tests, (2) serves Prometheus metrics at :9464/metrics. Prometheus scrapes it on its normal schedule. Everything else is plumbing you already have.

Exporter script

Minimal but production-adjacent:

import http from 'node:http';
import client from 'prom-client';

const KEY = process.env.INBOX_CHECK_API_KEY;
const BASE = 'https://check.live-direct-marketing.online';
const SENDERS = (process.env.SENDERS || '').split(',');
const INTERVAL_MIN = Number(process.env.INTERVAL_MIN || 60);

const reg = new client.Registry();
const inboxRate = new client.Gauge({
  name: 'inbox_check_inbox_rate',
  help: 'Inbox rate 0..1 by provider and sender',
  labelNames: ['sender', 'provider'],
  registers: [reg],
});
const dmarcPass = new client.Gauge({
  name: 'inbox_check_dmarc_pass',
  help: '1 if DMARC passes with alignment, else 0',
  labelNames: ['sender'],
  registers: [reg],
});
const dnsblCount = new client.Gauge({
  name: 'inbox_check_dnsbl_listings',
  help: 'Count of DNSBL listings detected',
  labelNames: ['sender'],
  registers: [reg],
});
const spamScore = new client.Gauge({
  name: 'inbox_check_spamassassin_score',
  help: 'SpamAssassin score (higher is worse)',
  labelNames: ['sender'],
  registers: [reg],
});

async function runOne(sender) {
  const start = await fetch(`${BASE}/api/check`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      senderDomain: sender,
      subject: 'Observability probe',
      html: '<p>metric probe</p>',
    }),
  }).then((r) => r.json());

  let res;
  for (let i = 0; i < 40; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    res = await fetch(`${BASE}/api/check/${start.id}`, {
      headers: { Authorization: `Bearer ${KEY}` },
    }).then((r) => r.json());
    if (res.status === 'complete') break;
  }
  if (!res || res.status !== 'complete') return;

  for (const p of res.providers) {
    inboxRate.set({ sender, provider: p.name }, p.inboxRate);
  }
  dmarcPass.set({ sender }, res.auth.dmarc === 'pass' ? 1 : 0);
  dnsblCount.set({ sender }, res.dnsbl?.listingsCount ?? 0);
  spamScore.set({ sender }, res.spamAssassinScore ?? 0);
}

setInterval(() => {
  for (const s of SENDERS) runOne(s).catch(console.error);
}, INTERVAL_MIN * 60_000);

http
  .createServer(async (req, res) => {
    if (req.url !== '/metrics') return res.end();
    res.setHeader('Content-Type', reg.contentType);
    res.end(await reg.metrics());
  })
  .listen(9464, () => console.log('exporter on :9464'));

Scrape config on the Prometheus side is the usual:

scrape_configs:
  - job_name: inbox-check
    scrape_interval: 60s
    static_configs:
      - targets: ['inbox-check-exporter.observability.svc:9464']

The dashboard JSON

A partial but representative excerpt — the full file is in the inbox-check/dashboards repo. Import via Grafana → Dashboards → New → Import, paste JSON, select your Prometheus datasource.

{
  "title": "Email Deliverability",
  "tags": ["deliverability", "email"],
  "schemaVersion": 39,
  "timezone": "",
  "panels": [
    {
      "type": "timeseries",
      "title": "Inbox rate by provider",
      "targets": [
        {
          "expr": "inbox_check_inbox_rate",
          "legendFormat": "{{sender}} · {{provider}}"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "percentunit",
          "min": 0,
          "max": 1,
          "thresholds": {
            "mode": "absolute",
            "steps": [
              { "color": "red",    "value": null },
              { "color": "orange", "value": 0.6  },
              { "color": "green",  "value": 0.8  }
            ]
          }
        }
      }
    },
    {
      "type": "stat",
      "title": "DMARC alignment (now)",
      "targets": [{ "expr": "inbox_check_dmarc_pass" }]
    },
    {
      "type": "bargauge",
      "title": "DNSBL listings",
      "targets": [{ "expr": "inbox_check_dnsbl_listings" }]
    }
  ]
}

Alerting rules

Two alerts cover most real incidents:

# inbox_rate_drop.yml  (Grafana Unified Alerting or Prometheus)
groups:
  - name: deliverability
    rules:
      - alert: InboxRateDrop
        expr: avg by (sender) (inbox_check_inbox_rate) < 0.8
        for: 15m
        labels:
          severity: warning
          team: growth
        annotations:
          summary: "Inbox rate below 80% on {{ $labels.sender }}"

      - alert: DNSBLListing
        expr: inbox_check_dnsbl_listings > 0
        for: 0m
        labels:
          severity: page
        annotations:
          summary: "{{ $labels.sender }} appears on {{ $value }} DNSBL(s)"

Panel ideas

  • Inbox rate over time, per provider. Your top panel. One line per (sender, provider) pair.
  • DMARC alignment heatmap. X-axis time, Y-axis sender, cell colour pass/fail. Spot patterns like "transactional fails alignment on the 15th of every month".
  • DNSBL listing count stat. Big red number when it matters.
  • SpamAssassin score time-series. Lets you catch slow content drift before it lands you in Spam.
  • Per-provider placement last run. Stat panels showing today's number vs yesterday's, with sparklines.
Instrument the exporter itself

Expose a counter for API calls, a histogram for API latency, and a gauge for your current quota usage. Deliverability silently turning off because you ran out of test credits is exactly the sort of thing an observability-first team should not miss.

Integrating with PagerDuty via Grafana

Grafana Unified Alerting has a PagerDuty contact point out of the box. Create one, paste your PagerDuty integration key, route the severity=page alerts to it and severity=warning to Slack. The DNSBL alert earlier is the classic page-worthy event: the moment one of your sending domains shows up on Spamhaus is the moment an on-call engineer wants to know.

Frequently asked questions

What if I already use Datadog / New Relic / Honeycomb?

The exporter pattern works the same. Replace prom-client with dd-trace or the OpenTelemetry SDK and ship metrics as gauges. The dashboard JSON is Grafana-specific but the metric shape is portable.

Does the exporter need to run in-cluster?

No. It only needs outbound HTTPS to the Inbox Check API and inbound HTTP to your Prometheus. A single small VM or a sidecar on an existing monitoring host is fine.

How fresh are the metrics?

As fresh as the interval you pick. The default of 60 minutes matches a nightly-monitor mental model and keeps API quota use reasonable. Drop to 15 minutes for high-volume senders; avoid going under 5 minutes to respect rate limits.

Can I filter by segment or campaign?

Add labels to the gauges. The exporter above only labels by sender and provider; extend it with a campaign label and include that in the POST payload, then facet dashboards on it.
Related reading
Found this useful? Share it
AB
About the author
Artem Berezin
B2B Deliverability Specialist

B2B deliverability specialist with 5+ years of hands-on outreach experience. Built campaigns reaching 90,000+ inboxes across 20+ countries — and fixed the deliverability problems that came with that scale.

Check your deliverability across 20+ providers

Gmail, Outlook, Yahoo, Mail.ru, Yandex, GMX, ProtonMail and more. Real inbox screenshots, SPF/DKIM/DMARC, spam engine verdicts. Free, no signup.

Run Free Test →

Unlimited tests · 20+ seed mailboxes · Live results · No account required