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
class DeliveryTagger
def self.delivering_email(message)
new(message).tag!
end
def initialize(message)
@message = message
end
def tag!
stamp("X-Delivery-Started-At", monotonic_now)
context = @message.instance_variable_get(:@_mailer_class)
if context
stamp("X-Mailer-Class", context.to_s)
end
if @message.respond_to?(:delivery_handler) && @message["X-Mailer-Action"].nil?
stamp("X-Mailer-Action", @message.header["X-Mailer-Action"]&.value || "deliver")
end
rescue => e
Rails.logger.warn("[DeliveryTagger] tagging failed: #{e.class}: #{e.message}")
end
private
def stamp(name, value)
@message.header[name] = value.to_s unless @message.header[name]
end
def monotonic_now
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
end
# config/initializers/mailer_observability.rb
ActiveSupport.on_load(:action_mailer) do
register_interceptor(DeliveryTagger)
register_observer(DeliveryObserver)
end
ActiveSupport::Notifications.subscribe("deliver.action_mailer") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
payload = event.payload
Metrics.timing(
"mailer.framework_deliver_ms",
event.duration.round,
tags: {
mailer: payload[:mailer] || "unknown",
message_id: payload[:message_id]
}
)
if payload[:exception]
Metrics.increment("mailer.delivery_error", tags: { mailer: payload[:mailer] })
Rails.logger.error("[mailer] delivery raised: #{payload[:exception].inspect}")
end
end
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
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
Share this code
Here's the card — post it anywhere.