ruby 100 lines · 3 tabs

Action Mailer Delivery Observability Hook

Shared by codesnips Jan 2026
3 tabs
class DeliveryObserver
  def self.delivered_email(message)
    new(message).record
  end

  def initialize(message)
    @message = message
  end

  def record
    Metrics.increment("mailer.delivered", tags: tags)
    Metrics.timing("mailer.delivery_duration_ms", elapsed_ms, tags: tags) if started_at
  rescue => e
    Rails.logger.warn("[DeliveryObserver] metrics failed: #{e.class}: #{e.message}")
  end

  private

  def tags
    { mailer: header_value("X-Mailer-Class") || "unknown",
      action: header_value("X-Mailer-Action") || "unknown",
      to_domain: recipient_domain }
  end

  def elapsed_ms
    ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round
  end

  def started_at
    raw = header_value("X-Delivery-Started-At")
    raw && raw.to_f
  end

  def recipient_domain
    to = Array(@message.to).first
    to.to_s.split("@").last || "unknown"
  end

  def header_value(name)
    field = @message.header[name]
    field && field.value
  end
end
3 files · ruby Explain with highlit

Action Mailer exposes two extension points that are perfect for observability without touching every mailer: register_observer and register_interceptor. This snippet wires both into a single delivery-tracking pipeline that records latency, success/failure counts, and per-message metadata, so email health becomes visible in dashboards instead of being a black box.

The DeliveryObserver shown in the first tab implements the observer contract, which is just a single delivered_email(message) method invoked after the underlying Mail::Message is handed to the delivery method. Because the observer runs post-delivery, it is the right place to emit a mailer.delivered counter tagged by mailer class and to record a delivery-duration timer. The mailer and action names are pulled from the custom X-Mailer-Class/X-Mailer-Action headers that the interceptor stamps earlier, with header[...] guarded because those headers may be absent on messages sent outside the normal mailer flow. A begin/rescue wraps the whole body so a metrics backend hiccup can never break real mail delivery — observability must be non-fatal.

The DeliveryTagger interceptor in the second tab runs before delivery. Interceptors can mutate the message, so it stamps a monotonic start time into X-Delivery-Started-At and copies the ActiveSupport delivery context (mailer class and action) into headers the observer can read back. This split matters: the interceptor has access to the outgoing message before the network call, while the observer sees the result afterward, and passing state between them through headers avoids thread-local juggling.

The third tab, mailer_observability.rb, is the initializer that registers both hooks once at boot via ActiveSupport.on_load(:action_mailer), ensuring registration happens after Action Mailer is fully loaded. It also subscribes to the deliver.action_mailer notification to capture the framework's own timing as a cross-check.

The main trade-off is that header-based state is visible on the wire, so nothing sensitive should be stashed there; the X-Delivery-Started-At clock value is harmless. This pattern is worth reaching for when email volume grows and silent failures — bounces, slow SMTP, misconfigured providers — need to surface as metrics and alerts rather than support tickets.


Related snips

Share this code

Here's the card — post it anywhere.

Action Mailer Delivery Observability Hook — share card
Link copied