Skip to content

Commit 5b68a5b

Browse files
feat: subscribe to process.action_mailer notifications (#1185)
* 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 <[email protected]>
1 parent c2ffafc commit 5b68a5b

File tree

6 files changed

+247
-13
lines changed

6 files changed

+247
-13
lines changed

instrumentation/action_mailer/README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ To use the instrumentation, call `use` with the name of the instrumentation:
2121

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

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

5050
### Options
5151

@@ -67,9 +67,9 @@ end
6767

6868
## Semantic Conventions
6969

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

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

7474
| Attribute Name | Type | Notes |
7575
| - | - | - |
@@ -79,7 +79,15 @@ The following attributes from the notification payload for the `deliver.action_m
7979
| `email.to.address` | Array | Receiver for mail (omit by default, include when `email_address` set to `:include`) |
8080
| `email.from.address` | Array | Sender for mail (omit by default, include when `email_address` set to `:include`) |
8181
| `email.cc.address` | Array | mail CC (omit by default, include when `email_address` set to `:include`) |
82-
| `email.bcc.address` | Array | mail BCC (omit by default, include when `email_address` set to `:include`) |
82+
| `email.bcc.address` | Array | mail BCC (omit by default, include when `email_address` set to `:include`) |
83+
84+
### Attributes attached to the `process.action_mailer` event payload
85+
86+
| Attribute Name | Type | Notes |
87+
| - | - | - |
88+
| `mailer` | String | Mailer class that is used to render the mail |
89+
| `action` | String | Method from the mailer class called to render the mail |
90+
| `args` | Array | Arguments passed to the method to render the email |
8391

8492
## Examples
8593

instrumentation/action_mailer/Rakefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Rake::TestTask.new :test do |t|
1515
t.libs << 'test'
1616
t.libs << 'lib'
1717
t.test_files = FileList['test/**/*_test.rb']
18+
t.warning = false
1819
end
1920

2021
YARD::Rake::YardocTask.new do |t|

instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/instrumentation.rb

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,53 @@
77
module OpenTelemetry
88
module Instrumentation
99
module ActionMailer
10-
# The Instrumentation class contains logic to detect and install the ActionMailer instrumentation
10+
# The {OpenTelemetry::Instrumentation::ActionMailer::Instrumentation} class contains logic to detect and install the ActionMailer instrumentation
11+
#
12+
# Installation and configuration of this instrumentation is done within the
13+
# {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry/SDK#configure-instance_method OpenTelemetry::SDK#configure}
14+
# block, calling {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry%2FSDK%2FConfigurator:use use()}
15+
# or {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry%2FSDK%2FConfigurator:use_all use_all()}.
16+
#
17+
# ## Configuration keys and options
18+
#
19+
# ### `:disallowed_notification_payload_keys`
20+
#
21+
# Specifies an array of keys that should be excluded from the `deliver.action_mailer` notification payload as span attributes.
22+
#
23+
# ### `:disallowed_process_payload_keys`
24+
#
25+
# Specifies an array of keys that should be excluded from the `process.action_mailer` notification payload as span attributes.
26+
#
27+
# ### `:notification_payload_transform`
28+
#
29+
# - `proc` **default** `nil`
30+
#
31+
# 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.
32+
#
33+
# ### `:process_payload_transform`
34+
#
35+
# - `proc` **default** `nil`
36+
#
37+
# 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.
38+
#
39+
# ### `:email_address`
40+
#
41+
# - `symbol` **default** `:omit`
42+
#
43+
# Specifies whether to include email addresses in the notification payload. Valid values are `:omit` and `:include`.
44+
#
45+
# @example An explicit default configuration
46+
# OpenTelemetry::SDK.configure do |c|
47+
# c.use_all({
48+
# 'OpenTelemetry::Instrumentation::ActionMailer' => {
49+
# disallowed_notification_payload_keys: [],
50+
# disallowed_process_payload_keys: [],
51+
# notification_payload_transform: nil,
52+
# process_payload_transform: nil,
53+
# email_address: :omit,
54+
# },
55+
# })
56+
# end
1157
class Instrumentation < OpenTelemetry::Instrumentation::Base
1258
MINIMUM_VERSION = Gem::Version.new('6.1.0')
1359
EMAIL_ATTRIBUTE = %w[email.to.address email.from.address email.cc.address email.bcc.address].freeze
@@ -27,7 +73,9 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base
2773
end
2874

2975
option :disallowed_notification_payload_keys, default: [], validate: :array
76+
option :disallowed_process_payload_keys, default: [], validate: :array
3077
option :notification_payload_transform, default: nil, validate: :callable
78+
option :process_payload_transform, default: nil, validate: :callable
3179
option :email_address, default: :omit, validate: %I[omit include]
3280

3381
private

instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/railtie.rb

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,39 @@
77
module OpenTelemetry
88
module Instrumentation
99
module ActionMailer
10-
SUBSCRIPTIONS = %w[
11-
deliver.action_mailer
12-
].freeze
10+
DELIVER_SUBSCRIPTION = 'deliver.action_mailer'
11+
PROCESS_SUBSCRIPTION = 'process.action_mailer'
1312

1413
# This Railtie sets up subscriptions to relevant ActionMailer notifications
1514
class Railtie < ::Rails::Railtie
1615
config.after_initialize do
1716
::OpenTelemetry::Instrumentation::ActiveSupport::Instrumentation.instance.install({})
17+
subscribe_to_deliver
18+
subscribe_to_process
19+
end
1820

19-
SUBSCRIPTIONS.each do |subscription_name|
20-
config = ActionMailer::Instrumentation.instance.config
21+
class << self
22+
def subscribe_to_deliver
2123
::OpenTelemetry::Instrumentation::ActiveSupport.subscribe(
2224
ActionMailer::Instrumentation.instance.tracer,
23-
subscription_name,
25+
DELIVER_SUBSCRIPTION,
2426
config[:notification_payload_transform],
2527
config[:disallowed_notification_payload_keys]
2628
)
2729
end
30+
31+
def subscribe_to_process
32+
::OpenTelemetry::Instrumentation::ActiveSupport.subscribe(
33+
ActionMailer::Instrumentation.instance.tracer,
34+
PROCESS_SUBSCRIPTION,
35+
config[:process_payload_transform],
36+
config[:disallowed_process_payload_keys]
37+
)
38+
end
39+
40+
def config
41+
ActionMailer::Instrumentation.instance.config
42+
end
2843
end
2944
end
3045
end
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
require 'test_helper'
8+
require 'opentelemetry-instrumentation-active_support'
9+
10+
describe OpenTelemetry::Instrumentation::ActionMailer do
11+
let(:exporter) { EXPORTER }
12+
let(:spans) { exporter.finished_spans }
13+
let(:instrumentation) { OpenTelemetry::Instrumentation::ActionMailer::Instrumentation.instance }
14+
15+
before do
16+
exporter.reset
17+
end
18+
19+
describe 'deliver.action_mailer' do
20+
describe 'with default configuration' do
21+
it 'generates a deliver span' do
22+
subscribing_to_deliver do
23+
TestMailer.hello_world.deliver_now
24+
end
25+
26+
_(spans.length).must_equal(1)
27+
span = spans.find { |s| s.name == 'deliver.action_mailer' }
28+
29+
_(span).wont_be_nil
30+
31+
_(span.attributes['email.x_mailer']).must_equal('TestMailer')
32+
_(span.attributes['email.subject']).must_equal('Hello world')
33+
_(span.attributes['email.message_id']).wont_be_empty
34+
end
35+
end
36+
37+
describe 'with custom configuration' do
38+
it 'with email_address: :include' do
39+
with_configuration(email_address: :include, disallowed_notification_payload_keys: []) do
40+
subscribing_to_deliver do
41+
TestMailer.hello_world.deliver_now
42+
end
43+
end
44+
45+
_(spans.length).must_equal(1)
46+
span = spans.find { |s| s.name == 'deliver.action_mailer' }
47+
48+
_(span).wont_be_nil
49+
50+
_(span.attributes['email.x_mailer']).must_equal('TestMailer')
51+
_(span.attributes['email.subject']).must_equal('Hello world')
52+
_(span.attributes['email.message_id']).wont_be_empty
53+
_(span.attributes['email.to.address']).must_equal(['[email protected]'])
54+
_(span.attributes['email.from.address']).must_equal(['[email protected]'])
55+
_(span.attributes['email.cc.address']).must_equal(['[email protected]'])
56+
_(span.attributes['email.bcc.address']).must_equal(['[email protected]'])
57+
end
58+
59+
it 'with a custom transform proc' do
60+
transform = ->(payload) { payload.transform_keys(&:upcase) }
61+
with_configuration(notification_payload_transform: transform) do
62+
instrumentation.send(:ecs_mail_convention)
63+
subscribing_to_deliver do
64+
TestMailer.hello_world.deliver_now
65+
end
66+
end
67+
68+
_(spans.length).must_equal(1)
69+
span = spans.find { |s| s.name == 'deliver.action_mailer' }
70+
71+
_(span).wont_be_nil
72+
73+
_(span.attributes['EMAIL.X_MAILER']).must_equal('TestMailer')
74+
_(span.attributes['EMAIL.SUBJECT']).must_equal('Hello world')
75+
_(span.attributes['EMAIL.MESSAGE_ID']).wont_be_empty
76+
end
77+
end
78+
end
79+
80+
describe 'process.action_mailer' do
81+
describe 'with default configuration' do
82+
it 'generates a process span' do
83+
transform = ->(payload) { payload.transform_keys(&:upcase) }
84+
with_configuration(disallowed_process_payload_keys: [:ARGS], process_payload_transform: transform) do
85+
subscribing_to_process do
86+
TestMailer.hello_world('Hola mundo').deliver_now
87+
end
88+
end
89+
90+
_(spans.length).must_equal(1)
91+
span = spans.find { |s| s.name == 'process.action_mailer' }
92+
93+
_(span).wont_be_nil
94+
95+
_(span.attributes['MAILER']).must_equal('TestMailer')
96+
_(span.attributes['ACTION']).must_equal('hello_world')
97+
_(span.attributes['ARGS']).must_be_nil
98+
end
99+
end
100+
101+
describe 'with custom configuration' do
102+
it 'generates a process span' do
103+
subscribing_to_process do
104+
TestMailer.hello_world('Hola mundo').deliver_now
105+
end
106+
107+
_(spans.length).must_equal(1)
108+
span = spans.find { |s| s.name == 'process.action_mailer' }
109+
110+
_(span).wont_be_nil
111+
112+
_(span.attributes['mailer']).must_equal('TestMailer')
113+
_(span.attributes['action']).must_equal('hello_world')
114+
_(span.attributes['args']).must_equal(['Hola mundo'])
115+
end
116+
end
117+
end
118+
119+
def with_configuration(values, &block)
120+
original_config = instrumentation.instance_variable_get(:@config)
121+
modified_config = original_config.merge(values)
122+
instrumentation.instance_variable_set(:@config, modified_config)
123+
124+
yield
125+
126+
instrumentation.instance_variable_set(:@config, original_config)
127+
end
128+
129+
def subscribing_to_deliver(&block)
130+
subscription = OpenTelemetry::Instrumentation::ActionMailer::Railtie.subscribe_to_deliver
131+
yield
132+
ensure
133+
ActiveSupport::Notifications.unsubscribe(subscription)
134+
end
135+
136+
def subscribing_to_process(&block)
137+
subscription = OpenTelemetry::Instrumentation::ActionMailer::Railtie.subscribe_to_process
138+
yield
139+
ensure
140+
ActiveSupport::Notifications.unsubscribe(subscription)
141+
end
142+
end

instrumentation/action_mailer/test/test_helper.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,23 @@
2222
c.use 'OpenTelemetry::Instrumentation::ActionMailer'
2323
c.add_span_processor span_processor
2424
end
25+
26+
OpenTelemetry::Instrumentation::ActiveSupport::Instrumentation.instance.install({})
27+
OpenTelemetry::Instrumentation::ActionMailer::Instrumentation.instance.install({})
28+
29+
ActionMailer::Base.delivery_method = :test
30+
31+
class TestMailer < ActionMailer::Base
32+
33+
34+
35+
36+
37+
def hello_world(message = 'Hello world')
38+
@message = message
39+
mail from: FROM, to: TO, cc: CC, bcc: BCC do |format|
40+
format.html { render inline: '<h1><%= @message %></h1>' }
41+
format.text { render inline: '<%= @message %>' }
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)