Skip to content

Commit

Permalink
Better document notifications feature (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
mullermp authored Oct 29, 2024
1 parent 1b6559c commit 60d8074
Show file tree
Hide file tree
Showing 17 changed files with 116 additions and 85 deletions.
5 changes: 4 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ AllCops:
- 'sample-app/**/*'
- 'sample_app_old/**/*'
- 'spec/dummy/**/*'
- 'test/dummy/**/*'
- 'spec/fixtures/**/*'
- 'spec/fixtures/**/*'
- 'db/**/*'
Expand All @@ -32,11 +33,13 @@ Style/GlobalVars:
Metrics/BlockLength:
Exclude:
- 'spec/**/*.rb'
- 'test/**/*.rb'
- aws-sdk-rails.gemspec

Metrics/ModuleLength:
Exclude:
- 'spec/**/*.rb'
- 'test/**/*.rb'

Style/HashSyntax:
EnforcedShorthandSyntax: never
Expand All @@ -45,5 +48,5 @@ Style/Documentation:
Exclude:
- 'lib/generators/**/*.rb'
- 'lib/aws/rails/notifications.rb'
- 'test/**/*.rb'
- 'spec/**/*.rb'
- 'test/**/*.rb'
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Unreleased Changes

* Issue - `ActionDispatch::Session::DynamoDbStore` now inherits `ActionDispatch::Session::AbstractStore` by wrapping `Aws::SessionStore::DynamoDB::RackMiddleware`.

* Issue - Do not skip autoload modules for `Aws::Rails.instrument_sdk_operations`.

4.1.0 (2024-09-27)
------------------

Expand Down
24 changes: 10 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,7 @@ class.

### Usage

To use the session store, add or edit your
`config/initializers/session_store.rb` file:
To use the session store, add or edit your `config/initializers/session_store.rb` file:

```ruby
options = { table_name: '_your_app_session' } # overrides from YAML or ENV
Expand Down Expand Up @@ -348,25 +347,22 @@ message['X-SES-FROM-ARN'] = 'arn:aws:ses:us-west-2:012345678910:identity/bigchun
message.deliver
```

## Active Support Notification Instrumentation for AWS SDK calls
To add `ActiveSupport::Notifications` Instrumentation to all AWS SDK client
operations call `Aws::Rails.instrument_sdk_operations` before you construct any
SDK clients.
## Active Support Notifications for AWS SDK calls

To add `ActiveSupport::Notifications` instrumentation to all AWS SDK client operations,
add or edit your `config/initializers/instrument_aws_sdk.rb` file:

Example usage in `config/initializers/instrument_aws_sdk.rb`
```ruby
Aws::Rails.instrument_sdk_operations
```

Events are published for each client operation call with the following event
name: <operation>.<serviceId>.aws. For example, S3's put_object has an event
name of: `put_object.S3.aws`. The service name will always match the
namespace of the service client (eg Aws::S3::Client => 'S3').
The payload of the event is the
Events are published for each client operation call with the following event name:
`<operation>.<serviceId>.aws`. For example, S3's `:put_object` has an event name
of: `put_object.S3.aws`. The service name will always match the namespace of the
service client (e.g. Aws::S3::Client => 'S3'). The payload of the event is the
[request context](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Seahorse/Client/RequestContext.html).

You can subscribe to these events as you would other
`ActiveSupport::Notifications`:
You can subscribe to these events as you would for other `ActiveSupport::Notifications`:

```ruby
ActiveSupport::Notifications.subscribe('put_object.S3.aws') do |name, start, finish, id, payload|
Expand Down
8 changes: 2 additions & 6 deletions lib/aws/rails/notifications.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@

module Aws
module Rails
# Instruments client operation calls for ActiveSupport::Notifications
# Each client operation will produce an event with name:
# <operation>.<service>.aws
# @api private
class Notifications < Seahorse::Client::Plugin
# This plugin needs to be first, which means it is called first in the stack,
# to start recording time, and returns last
def add_handlers(handlers, _config)
# This plugin needs to be first
# which means it is called first in the stack, to start recording time,
# and returns last
handlers.add(Handler, step: :initialize, priority: 99)
end

Expand Down
51 changes: 24 additions & 27 deletions lib/aws/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,17 @@ class Railtie < ::Rails::Railtie
initializer 'aws-sdk-rails.initialize',
before: :load_config_initializers do
# Initialization Actions
Aws::Rails.log_to_rails_logger
Aws::Rails.use_rails_encrypted_credentials
Aws::Rails.add_action_mailer_delivery_method
Aws::Rails.add_action_mailer_delivery_method(:sesv2)
Aws::Rails.log_to_rails_logger
end

initializer 'aws-sdk-rails.insert_middleware' do |app|
Aws::Rails.add_sqsd_middleware(app)
end

initializer 'aws-sdk-rails.sdk_eager_load' do
config.before_eager_load do
config.eager_load_namespaces << Aws
end

initializer 'aws-sdk-rails.eager_load' do
Aws.define_singleton_method(:eager_load!) do
Aws.constants.each do |c|
m = Aws.const_get(c)
Expand All @@ -33,6 +29,10 @@ class Railtie < ::Rails::Railtie
end
end
end

config.before_eager_load do
config.eager_load_namespaces << Aws
end
end

rake_tasks do
Expand All @@ -41,6 +41,20 @@ class Railtie < ::Rails::Railtie
end
end

# Configures the AWS SDK for Ruby's logger to use the Rails logger.
def self.log_to_rails_logger
Aws.config[:logger] = ::Rails.logger
nil
end

# Configures the AWS SDK with credentials from Rails encrypted credentials.
def self.use_rails_encrypted_credentials
# limit the config keys we merge to credentials only
aws_credential_keys = %i[access_key_id secret_access_key session_token account_id]
creds = ::Rails.application.credentials[:aws].to_h.slice(*aws_credential_keys)
Aws.config.merge!(creds)
end

# This is called automatically from the SDK's Railtie, but can be manually
# called if you want to specify options for building the Aws::SES::Client or
# Aws::SESV2::Client.
Expand All @@ -61,31 +75,14 @@ def self.add_action_mailer_delivery_method(name = :ses, client_options = {})
end
end

# Configures the AWS SDK for Ruby's logger to use the Rails logger.
def self.log_to_rails_logger
Aws.config[:logger] = ::Rails.logger
nil
end

# Configures the AWS SDK with credentials from Rails encrypted credentials.
def self.use_rails_encrypted_credentials
# limit the config keys we merge to credentials only
aws_credential_keys = %i[access_key_id secret_access_key session_token account_id]
creds = ::Rails.application.credentials[:aws].to_h.slice(*aws_credential_keys)
Aws.config.merge!(creds)
end

# Adds ActiveSupport Notifications instrumentation to AWS SDK
# client operations. Each operation will produce an event with a name:
# <operation>.<service>.aws. For example, S3's put_object has an event
# name of: put_object.S3.aws
# Add ActiveSupport Notifications instrumentation to AWS SDK client operations.
# Each operation will produce an event with a name `<operation>.<service>.aws`.
# For example, S3's put_object has an event name of: put_object.S3.aws
def self.instrument_sdk_operations
Aws.constants.each do |c|
next if Aws.autoload?(c)

m = Aws.const_get(c)
if m.is_a?(Module) && m.const_defined?(:Client) &&
m.const_get(:Client).superclass == Seahorse::Client::Base
(client = m.const_get(:Client)) && client.superclass == Seahorse::Client::Base
m.const_get(:Client).add_plugin(Aws::Rails::Notifications)
end
end
Expand Down
27 changes: 26 additions & 1 deletion sample-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Run `EDITOR=nano bundle exec rails credentials:edit` to edit credentials.

Commented credentials are defined under the `:aws` key. Uncomment the credentials, which should look like:

```
```yaml
aws:
access_key_id: secret
secret_access_key: akid
Expand All @@ -44,6 +44,31 @@ Run `bundle exec rails console` to start the console.

Inspect the output of `Aws.config` and ensure the credentials are set.

## ActiveSupport Notifications

### Setup

This is configured in `config/initializers/instrument_aws_sdk.rb`. See the `aws-sdk-rails` README.

`UsersController#index` captures any AWS SDK notification with:

```ruby
ActiveSupport::Notifications.subscribe(/[.]aws/) do |name, start, finish, id, _payload|
Rails.logger.info "Got notification: #{name} #{start} #{finish} #{id}"
end
```

### Testing

Start the service with `bundle exec rails server` and visit `http://127.0.0.1:3000/users`.

In the logs, you should at least see a notification for DynamoDB `update_item` from the session store.
It should look like:

```
Got notification: update_item.DynamoDB.aws ...
```

## DynamoDB Session Store

### Setup
Expand Down
3 changes: 3 additions & 0 deletions sample-app/app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ class UsersController < ApplicationController

# GET /users
def index
ActiveSupport::Notifications.subscribe(/[.]aws/) do |name, start, finish, id, _payload|
Rails.logger.info "Got notification: #{name} #{start} #{finish} #{id}"
end
@users = User.all
end

Expand Down
1 change: 1 addition & 0 deletions sample-app/config/initializers/instrument_aws_sdk.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Aws::Rails.instrument_sdk_operations
9 changes: 0 additions & 9 deletions sample_app_old/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,6 @@ Make sure your email address is verified in SES.

Fixture based testing of SES is possible via RSpec request helpers that this gem offers. How to use them is documented within the main README. How to setup inbound emails with SES is also covered there.

## ActiveSupport Notifications

ActiveSupport notifications for AWS clients are configured in
`config/initializers/instrument_aws_sdk/rb` to log an event
whenever an AWS client makes any service calls. To demo, follow
any one of the ActiveStorage, SES or SQS ActiveJob and the
AWS calls should be logged with:
`Recieved an ActiveSupport::Notification for: send_message.SQS.aws event`

## SQS ActiveJob

* Start rails with `AWS_ACTIVE_JOB_QUEUE_URL=https://my_sqs_queue_url rails server`
Expand Down
53 changes: 27 additions & 26 deletions spec/aws/rails/railtie_spec.rb → test/aws/rails/railtie_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,41 @@

require 'test_helper'

require 'aws-sdk-core'

module Aws
# Test services namespaces
module Service1
Client = Aws::SES::Client.dup
# Test service for Notifications
# rubocop:disable Lint/EmptyClass
module Service
class Client < Seahorse::Client::Base; end
end

module Service2
Client = Aws::SES::Client.dup
module NotService
class Client; end
end

class Client; end
# rubocop:enable Lint/EmptyClass

module Rails
describe 'Railtie' do
it 'adds action mailer delivery method' do
it 'uses aws credentials from rails encrypted credentials' do
rails_creds = ::Rails.application.credentials.aws
expect(Aws.config[:access_key_id]).to eq rails_creds[:access_key_id]
expect(Aws.config[:secret_access_key]).to eq rails_creds[:secret_access_key]
expect(Aws.config[:session_token]).to eq rails_creds[:session_token]
expect(Aws.config[:account_id]).to eq rails_creds[:account_id]

expect(rails_creds[:something]).not_to be_nil
expect(Aws.config[:something]).to be_nil
end

it 'adds action mailer delivery methods' do
expect(ActionMailer::Base.delivery_methods[:ses]).to eq Aws::Rails::SesMailer
expect(ActionMailer::Base.delivery_methods[:sesv2]).to eq Aws::Rails::Sesv2Mailer
end

it 'sets the Aws logger' do
it 'sets the Rails logger to Aws global config' do
expect(Aws.config[:logger]).to eq ::Rails.logger
end

Expand All @@ -28,27 +45,11 @@ module Rails
expect(::Rails.application.config.eager_load_namespaces).to include(Aws)
end

describe '.use_rails_encrypted_credentials' do
let(:rails_creds) { ::Rails.application.credentials.aws }

it 'sets aws credentials' do
expect(Aws.config[:access_key_id]).to eq rails_creds[:access_key_id]
expect(Aws.config[:secret_access_key]).to eq rails_creds[:secret_access_key]
end

it 'does not load non credential keys into aws config' do
expect(rails_creds[:non_credential_key]).not_to be_nil
expect(Aws.config[:non_credential_key]).to be_nil
end
end

describe '.instrument_sdk_operations' do
it 'adds the Notifications plugin to sdk clients' do
expect(Aws::Service1::Client).to receive(:add_plugin).with(Aws::Rails::Notifications)
expect(Aws::Service2::Client).to receive(:add_plugin).with(Aws::Rails::Notifications)

# Ensure other Clients don't get plugin added
allow_any_instance_of(Class).to receive(:add_plugin)
expect(Aws::Service::Client).to receive(:add_plugin).with(Aws::Rails::Notifications)
expect(Aws::NotService::Client).not_to receive(:add_plugin)
expect(Aws::Client).not_to receive(:add_plugin)

Aws::Rails.instrument_sdk_operations
end
Expand Down
4 changes: 4 additions & 0 deletions test/dummy/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source "https://rubygems.org"

gem "aws-sdk-rails", path: "../../"
gem "rails"
4 changes: 4 additions & 0 deletions test/dummy/bin/rails
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../config/boot"
require "rails/commands"
5 changes: 4 additions & 1 deletion test/dummy/config/application.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# frozen_string_literal: true

require 'rails'
require 'action_controller/railtie'
require 'action_mailer/railtie'

require 'aws-sdk-rails'
require 'aws-sessionstore-dynamodb'

module Dummy
class Application < Rails::Application
config.load_defaults Rails::VERSION::STRING.to_f
config.eager_load = false
config.eager_load = true
config.secret_key_base = 'secret'
end
end
1 change: 1 addition & 0 deletions test/dummy/config/boot.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'bundler/setup'
1 change: 1 addition & 0 deletions test/dummy/config/credentials.yml.enc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6bBVcp2SyvttZcv4ATnSjW73mFUklcvQvoN1atdK0NdmKyNZwas37cSuFSaKFoco/Thf7xrUsiN2cxWYUO9qui0wV5bdTJ5MdpGU05Z9f0jcXp6sPGANc7P/j7GeVFoVVxnNgtIcE4ksx0dQwjlN1/zE7ziihPJPG5OO0g/IEgxs2vMJS1zDLScBmc+P+6D6KjxekjRyeonZ5a+pK9twiHhz0kps7ZS5Jql+7e6sB1SIBWMKiBHc/y5T/BNa0oK0Tj9Y/lMVrNk4O1jXXU1q2YCc7jXDaY8StKXDYUf4GGwkmKM9Ri91s/6+S0FqByV08ZET/u8YEaLYrsj++uSL9sgJ3Kc9AdAoK/oxT9XrdsP0ibm2zx/Jv8GojGpJWuOdMRNshcfCOl2EeQNGMlZhh6gJ+HJZXQcWo+oe4idLtVcn9zd4Lu53uxVZg5PycvKsBfGNbpExnXmG6kBMxY6svdaXvhdmtXHPAIMByu4WnIVb29kWFQ==--Ijdq6LGvh2aVS+yb--Ck2PDvGXuw9Ok9IUfv1N2A==
1 change: 1 addition & 0 deletions test/dummy/config/master.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
65a3ce664efa1804645dc56e1759903f
2 changes: 2 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

require 'minitest/autorun'
require 'minitest/unit'
require 'rspec/expectations/minitest_integration'
require 'rspec/mocks/minitest_integration'
require 'minitest-spec-rails'

ENV['RAILS_ENV'] = 'test'
Expand Down

0 comments on commit 60d8074

Please sign in to comment.