diff --git a/instrumentation/action_mailer/README.md b/instrumentation/action_mailer/README.md index 63c13d810..3d429e7d2 100644 --- a/instrumentation/action_mailer/README.md +++ b/instrumentation/action_mailer/README.md @@ -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' @@ -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 @@ -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 | | - | - | - | @@ -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 diff --git a/instrumentation/action_mailer/Rakefile b/instrumentation/action_mailer/Rakefile index 1a64ba842..4b0e9b5a8 100644 --- a/instrumentation/action_mailer/Rakefile +++ b/instrumentation/action_mailer/Rakefile @@ -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| diff --git a/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/instrumentation.rb b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/instrumentation.rb index 2b33478aa..3ba3cb714 100644 --- a/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/instrumentation.rb +++ b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/instrumentation.rb @@ -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 @@ -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 diff --git a/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/railtie.rb b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/railtie.rb index 0b036d0e9..724e2a162 100644 --- a/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/railtie.rb +++ b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/railtie.rb @@ -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 diff --git a/instrumentation/action_mailer/test/opentelemetry/instrumentation/action_mailer/subscription_test.rb b/instrumentation/action_mailer/test/opentelemetry/instrumentation/action_mailer/subscription_test.rb new file mode 100644 index 000000000..2d22098c6 --- /dev/null +++ b/instrumentation/action_mailer/test/opentelemetry/instrumentation/action_mailer/subscription_test.rb @@ -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 diff --git a/instrumentation/action_mailer/test/test_helper.rb b/instrumentation/action_mailer/test/test_helper.rb index b68d22b82..945016422 100644 --- a/instrumentation/action_mailer/test/test_helper.rb +++ b/instrumentation/action_mailer/test/test_helper.rb @@ -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: '

<%= @message %>

' } + format.text { render inline: '<%= @message %>' } + end + end +end