PhoenixDKIM

A security-focused DKIM signing and verification milter.


Metrics and observability

PhoenixDKIM maintains a small set of in-process counters at negligible cost and can expose them two ways: a Prometheus text file (pull) and a StatsD UDP stream (push). Both are pure POSIX — no extra library is linked and no compile flag is required — so they are switched on by configuration alone. There is also a per-message structured log line for log-based tooling.

If you run an OpenTelemetry stack, you do not need a native OTel exporter: the OpenTelemetry Collector has both a prometheus receiver and a statsd receiver, so point it at either output below and route onward to Datadog, Grafana Cloud, New Relic, etc.

Per-message log line

Set LogResults yes (with Syslog and/or StdoutLog enabled). In addition to the per-signature detail it already logs, PhoenixDKIM emits one structured key=value summary per message:

<jobid>: summary action=verify result=pass d=example.com a=rsa-sha256 sigs=1
<jobid>: summary action=sign d=example.com a=rsa-sha256

The signing domain and algorithm live here rather than in metric labels, on purpose — see "A note on labels" below.

Prometheus

MetricsFile      /var/lib/prometheus/node-exporter/phoenixdkim.prom
MetricsInterval  15        # seconds; default 15

A background thread writes atomically (temp file + rename(2)) every MetricsInterval seconds and once more on clean shutdown. The file lives directly in the Prometheus node_exporter textfile collector directory, which node_exporter watches by default — no extra flag and no network listener in the daemon.

On Debian/Ubuntu the textfile directory is /var/lib/prometheus/node-exporter; on other distributions it is commonly /var/lib/node_exporter/textfile_collector. The PhoenixDKIM systemd unit already has ReadWritePaths= set for both. If you configure MetricsFile to a different path, add the directory via a drop-in:

systemctl edit phoenixdkim.service
# add under [Service]:
# ReadWritePaths=/your/custom/path

HTTP endpoint (pull, no textfile collector)

If you would rather have Prometheus scrape PhoenixDKIM directly — no node_exporter, no textfile collector, no shared directory and its permissions dance — enable the built-in endpoint instead of (or alongside) MetricsFile:

MetricsAddr  127.0.0.1:9323     # default port 9323; [::1]:9323 for IPv6

A dedicated accept thread answers GET /metrics with the same exposition text and returns 404 for every other path. It speaks HTTP/1.0 and closes after each response (no keep-alive); a Prometheus scrape is one request per interval, so there is nothing to gain from connection reuse. No new library is linked.

There is no authentication or TLS — bind to a loopback or a dedicated management address and let Prometheus reach it over a trusted network (or front it with a reverse proxy if you need auth/TLS). A matching scrape config:

scrape_configs:
  - job_name: phoenixdkim
    static_configs:
      - targets: ['127.0.0.1:9323']

StatsD

StatsDHost    127.0.0.1:8125    # default port 8125; [::1]:8125 for IPv6
StatsDPrefix  phoenixdkim       # default

Events are pushed as StatsD counters/timers over a non-blocking UDP socket; packets drop silently if the collector is down, so a stalled collector never throttles mail flow. StatsD has no labels, so label values are folded into dotted names, e.g. phoenixdkim.verifications.pass:1|c.

Exported series

Metric Type Labels
phoenixdkim_build_info gauge version
phoenixdkim_messages_total counter
phoenixdkim_signatures_total counter result (ok/error), algorithm
phoenixdkim_verifications_total counter result (pass/fail/none/neutral/policy/temperror/permerror)
phoenixdkim_dns_queries_total counter
phoenixdkim_dns_responses_total counter result (success/timeout/error)
phoenixdkim_dns_duration_seconds histogram

verifications_total labels mirror the RFC 8601 dkim= result vocabulary, so the metric and the Authentication-Results header agree. The DNS series reflect the asynchronous libunbound resolver; the file-based test resolver (TestPublicKeys/TestDNSData) is not instrumented.

A note on labels

High-cardinality values — the signing domain (d=), selector, job ID — are deliberately not used as metric labels. Each distinct label value is a new Prometheus time series, and an unbounded set (one per signing domain on a busy signer) will overwhelm the store. Per-domain breakdowns belong in the LogResults summary line, which feeds log-based tools that handle high cardinality well.

Example PromQL

# inbound verification pass rate over 5m
sum(rate(phoenixdkim_verifications_total{result="pass"}[5m]))
  / sum(rate(phoenixdkim_verifications_total[5m]))

# 95th-percentile DNS key-lookup latency
histogram_quantile(0.95,
  sum(rate(phoenixdkim_dns_duration_seconds_bucket[5m])) by (le))

# signing errors per second
sum(rate(phoenixdkim_signatures_total{result="error"}[5m]))