Skip to content

Commit

Permalink
feat: subscribe to process.action_mailer notifications (#1185)
Browse files Browse the repository at this point in the history
* feat: subscribe to process.action_mailer notifications

* doc: add payload table for `process.action_mailer` event

* doc: expand ActionMailer::Instrumentation class docs

* tests: add test that execute an ActionMailer delivery

* tests: disable warnings

---------

Co-authored-by: Kayla Reopelle <87386821+kaylareopelle@users.noreply.github.com>
  • Loading branch information
mrsimo and kaylareopelle authored Oct 22, 2024
1 parent c2ffafc commit 5b68a5b
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 13 deletions.
20 changes: 14 additions & 6 deletions instrumentation/action_mailer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ To use the instrumentation, call `use` with the name of the instrumentation:

```ruby
OpenTelemetry::SDK.configure do |c|
# Use only the ActionMailer instrumentation
# Use only the ActionMailer instrumentation
c.use 'OpenTelemetry::Instrumentation::ActionMailer'
# Use the ActionMailer instrumentation along with the rest of the Rails-related instrumentation
c.use 'OpenTelemetry::Instrumentation::Rails'
Expand All @@ -44,8 +44,8 @@ See the table below for details of what [Rails Framework Hook Events](https://gu

| Event Name | Creates Span? | Notes |
| - | - | - |
| `deliver.action_mailer` | :white_check_mark: | Creates an span with kind `internal` and email content and status|
| `process.action_mailer` | :x: | Lack of useful info so ignored |
| `deliver.action_mailer` | :white_check_mark: | Creates a span with kind `internal` and email content and status |
| `process.action_mailer` | :white_check_mark: | Creates a span with kind `internal` that will include email rendering spans |

### Options

Expand All @@ -67,9 +67,9 @@ end

## Semantic Conventions

Internal spans are named using the name of the `ActiveSupport` event that was provided (e.g. `action_mailer deliver`).
Internal spans are named using the name of the `ActiveSupport` event that was provided (e.g. `deliver.action_mailer`).

The following attributes from the notification payload for the `deliver.action_mailer` event are attached to `action_mailer deliver` spans:
### Attributes attached to the `deliver.action_mailer` event payload

| Attribute Name | Type | Notes |
| - | - | - |
Expand All @@ -79,7 +79,15 @@ The following attributes from the notification payload for the `deliver.action_m
| `email.to.address` | Array | Receiver for mail (omit by default, include when `email_address` set to `:include`) |
| `email.from.address` | Array | Sender for mail (omit by default, include when `email_address` set to `:include`) |
| `email.cc.address` | Array | mail CC (omit by default, include when `email_address` set to `:include`) |
| `email.bcc.address` | Array | mail BCC (omit by default, include when `email_address` set to `:include`) |
| `email.bcc.address` | Array | mail BCC (omit by default, include when `email_address` set to `:include`) |

### Attributes attached to the `process.action_mailer` event payload

| Attribute Name | Type | Notes |
| - | - | - |
| `mailer` | String | Mailer class that is used to render the mail |
| `action` | String | Method from the mailer class called to render the mail |
| `args` | Array | Arguments passed to the method to render the email |

## Examples

Expand Down
1 change: 1 addition & 0 deletions instrumentation/action_mailer/Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Rake::TestTask.new :test do |t|
t.libs << 'test'
t.libs << 'lib'
t.test_files = FileList['test/**/*_test.rb']
t.warning = false
end

YARD::Rake::YardocTask.new do |t|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,53 @@
module OpenTelemetry
module Instrumentation
module ActionMailer
# The Instrumentation class contains logic to detect and install the ActionMailer instrumentation
# The {OpenTelemetry::Instrumentation::ActionMailer::Instrumentation} class contains logic to detect and install the ActionMailer instrumentation
#
# Installation and configuration of this instrumentation is done within the
# {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry/SDK#configure-instance_method OpenTelemetry::SDK#configure}
# block, calling {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry%2FSDK%2FConfigurator:use use()}
# or {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry%2FSDK%2FConfigurator:use_all use_all()}.
#
# ## Configuration keys and options
#
# ### `:disallowed_notification_payload_keys`
#
# Specifies an array of keys that should be excluded from the `deliver.action_mailer` notification payload as span attributes.
#
# ### `:disallowed_process_payload_keys`
#
# Specifies an array of keys that should be excluded from the `process.action_mailer` notification payload as span attributes.
#
# ### `:notification_payload_transform`
#
# - `proc` **default** `nil`
#
# Specifies custom proc used to extract span attributes form the `deliver.action_mailer` notification payload. Use this to rename keys, extract nested values, or perform any other custom logic.
#
# ### `:process_payload_transform`
#
# - `proc` **default** `nil`
#
# Specifies custom proc used to extract span attributes form the `process.action_mailer` notification payload. Use this to rename keys, extract nested values, or perform any other custom logic.
#
# ### `:email_address`
#
# - `symbol` **default** `:omit`
#
# Specifies whether to include email addresses in the notification payload. Valid values are `:omit` and `:include`.
#
# @example An explicit default configuration
# OpenTelemetry::SDK.configure do |c|
# c.use_all({
# 'OpenTelemetry::Instrumentation::ActionMailer' => {
# disallowed_notification_payload_keys: [],
# disallowed_process_payload_keys: [],
# notification_payload_transform: nil,
# process_payload_transform: nil,
# email_address: :omit,
# },
# })
# end
class Instrumentation < OpenTelemetry::Instrumentation::Base
MINIMUM_VERSION = Gem::Version.new('6.1.0')
EMAIL_ATTRIBUTE = %w[email.to.address email.from.address email.cc.address email.bcc.address].freeze
Expand All @@ -27,7 +73,9 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
end

option :disallowed_notification_payload_keys, default: [], validate: :array
option :disallowed_process_payload_keys, default: [], validate: :array
option :notification_payload_transform, default: nil, validate: :callable
option :process_payload_transform, default: nil, validate: :callable
option :email_address, default: :omit, validate: %I[omit include]

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,39 @@
module OpenTelemetry
module Instrumentation
module ActionMailer
SUBSCRIPTIONS = %w[
deliver.action_mailer
].freeze
DELIVER_SUBSCRIPTION = 'deliver.action_mailer'
PROCESS_SUBSCRIPTION = 'process.action_mailer'

# This Railtie sets up subscriptions to relevant ActionMailer notifications
class Railtie < ::Rails::Railtie
config.after_initialize do
::OpenTelemetry::Instrumentation::ActiveSupport::Instrumentation.instance.install({})
subscribe_to_deliver
subscribe_to_process
end

SUBSCRIPTIONS.each do |subscription_name|
config = ActionMailer::Instrumentation.instance.config
class << self
def subscribe_to_deliver
::OpenTelemetry::Instrumentation::ActiveSupport.subscribe(
ActionMailer::Instrumentation.instance.tracer,
subscription_name,
DELIVER_SUBSCRIPTION,
config[:notification_payload_transform],
config[:disallowed_notification_payload_keys]
)
end

def subscribe_to_process
::OpenTelemetry::Instrumentation::ActiveSupport.subscribe(
ActionMailer::Instrumentation.instance.tracer,
PROCESS_SUBSCRIPTION,
config[:process_payload_transform],
config[:disallowed_process_payload_keys]
)
end

def config
ActionMailer::Instrumentation.instance.config
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

require 'test_helper'
require 'opentelemetry-instrumentation-active_support'

describe OpenTelemetry::Instrumentation::ActionMailer do
let(:exporter) { EXPORTER }
let(:spans) { exporter.finished_spans }
let(:instrumentation) { OpenTelemetry::Instrumentation::ActionMailer::Instrumentation.instance }

before do
exporter.reset
end

describe 'deliver.action_mailer' do
describe 'with default configuration' do
it 'generates a deliver span' do
subscribing_to_deliver do
TestMailer.hello_world.deliver_now
end

_(spans.length).must_equal(1)
span = spans.find { |s| s.name == 'deliver.action_mailer' }

_(span).wont_be_nil

_(span.attributes['email.x_mailer']).must_equal('TestMailer')
_(span.attributes['email.subject']).must_equal('Hello world')
_(span.attributes['email.message_id']).wont_be_empty
end
end

describe 'with custom configuration' do
it 'with email_address: :include' do
with_configuration(email_address: :include, disallowed_notification_payload_keys: []) do
subscribing_to_deliver do
TestMailer.hello_world.deliver_now
end
end

_(spans.length).must_equal(1)
span = spans.find { |s| s.name == 'deliver.action_mailer' }

_(span).wont_be_nil

_(span.attributes['email.x_mailer']).must_equal('TestMailer')
_(span.attributes['email.subject']).must_equal('Hello world')
_(span.attributes['email.message_id']).wont_be_empty
_(span.attributes['email.to.address']).must_equal(['to@example.com'])
_(span.attributes['email.from.address']).must_equal(['from@example.com'])
_(span.attributes['email.cc.address']).must_equal(['cc@example.com'])
_(span.attributes['email.bcc.address']).must_equal(['bcc@example.com'])
end

it 'with a custom transform proc' do
transform = ->(payload) { payload.transform_keys(&:upcase) }
with_configuration(notification_payload_transform: transform) do
instrumentation.send(:ecs_mail_convention)
subscribing_to_deliver do
TestMailer.hello_world.deliver_now
end
end

_(spans.length).must_equal(1)
span = spans.find { |s| s.name == 'deliver.action_mailer' }

_(span).wont_be_nil

_(span.attributes['EMAIL.X_MAILER']).must_equal('TestMailer')
_(span.attributes['EMAIL.SUBJECT']).must_equal('Hello world')
_(span.attributes['EMAIL.MESSAGE_ID']).wont_be_empty
end
end
end

describe 'process.action_mailer' do
describe 'with default configuration' do
it 'generates a process span' do
transform = ->(payload) { payload.transform_keys(&:upcase) }
with_configuration(disallowed_process_payload_keys: [:ARGS], process_payload_transform: transform) do
subscribing_to_process do
TestMailer.hello_world('Hola mundo').deliver_now
end
end

_(spans.length).must_equal(1)
span = spans.find { |s| s.name == 'process.action_mailer' }

_(span).wont_be_nil

_(span.attributes['MAILER']).must_equal('TestMailer')
_(span.attributes['ACTION']).must_equal('hello_world')
_(span.attributes['ARGS']).must_be_nil
end
end

describe 'with custom configuration' do
it 'generates a process span' do
subscribing_to_process do
TestMailer.hello_world('Hola mundo').deliver_now
end

_(spans.length).must_equal(1)
span = spans.find { |s| s.name == 'process.action_mailer' }

_(span).wont_be_nil

_(span.attributes['mailer']).must_equal('TestMailer')
_(span.attributes['action']).must_equal('hello_world')
_(span.attributes['args']).must_equal(['Hola mundo'])
end
end
end

def with_configuration(values, &block)
original_config = instrumentation.instance_variable_get(:@config)
modified_config = original_config.merge(values)
instrumentation.instance_variable_set(:@config, modified_config)

yield

instrumentation.instance_variable_set(:@config, original_config)
end

def subscribing_to_deliver(&block)
subscription = OpenTelemetry::Instrumentation::ActionMailer::Railtie.subscribe_to_deliver
yield
ensure
ActiveSupport::Notifications.unsubscribe(subscription)
end

def subscribing_to_process(&block)
subscription = OpenTelemetry::Instrumentation::ActionMailer::Railtie.subscribe_to_process
yield
ensure
ActiveSupport::Notifications.unsubscribe(subscription)
end
end
20 changes: 20 additions & 0 deletions instrumentation/action_mailer/test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,23 @@
c.use 'OpenTelemetry::Instrumentation::ActionMailer'
c.add_span_processor span_processor
end

OpenTelemetry::Instrumentation::ActiveSupport::Instrumentation.instance.install({})
OpenTelemetry::Instrumentation::ActionMailer::Instrumentation.instance.install({})

ActionMailer::Base.delivery_method = :test

class TestMailer < ActionMailer::Base
FROM = 'from@example.com'
TO = 'to@example.com'
CC = 'cc@example.com'
BCC = 'bcc@example.com'

def hello_world(message = 'Hello world')
@message = message
mail from: FROM, to: TO, cc: CC, bcc: BCC do |format|
format.html { render inline: '<h1><%= @message %></h1>' }
format.text { render inline: '<%= @message %>' }
end
end
end

0 comments on commit 5b68a5b

Please sign in to comment.