Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract ActiveSupport from Rails #747

Merged
merged 8 commits into from
Sep 3, 2019
12 changes: 12 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ elsif Gem::Version.new('2.2.0') <= Gem::Version.new(RUBY_VERSION) \
end

appraise 'rails5-postgres-redis' do
gem 'rails', '~> 5.2.1'
gem 'pg', '< 1.0', platform: :ruby
gem 'redis'
end

appraise 'rails5-postgres-redis-activesupport' do
gem 'rails', '~> 5.2.1'
gem 'pg', '< 1.0', platform: :ruby
gem 'redis-rails'
Expand Down Expand Up @@ -425,6 +431,12 @@ elsif Gem::Version.new('2.3.0') <= Gem::Version.new(RUBY_VERSION) \
end

appraise 'rails5-postgres-redis' do
gem 'rails', '~> 5.2.1'
gem 'pg', '< 1.0', platform: :ruby
gem 'redis'
end

appraise 'rails5-postgres-redis-activesupport' do
gem 'rails', '~> 5.2.1'
gem 'pg', '< 1.0', platform: :ruby
gem 'redis-rails'
Expand Down
2 changes: 2 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ task :ci do
sh 'bundle exec appraisal rails5-mysql2 rake test:rails'
sh 'bundle exec appraisal rails5-postgres rake test:rails'
sh 'bundle exec appraisal rails5-postgres-redis rake test:railsredis'
sh 'bundle exec appraisal rails5-postgres-redis-activesupport rake test:railsredis'
sh 'bundle exec appraisal rails5-postgres-sidekiq rake test:railssidekiq'
sh 'bundle exec appraisal rails5-postgres-sidekiq rake test:railsactivejob'
sh 'bundle exec appraisal rails5-postgres rake test:railsdisableenv'
Expand Down Expand Up @@ -409,6 +410,7 @@ task :ci do
sh 'bundle exec appraisal rails5-mysql2 rake test:rails'
sh 'bundle exec appraisal rails5-postgres rake test:rails'
sh 'bundle exec appraisal rails5-postgres-redis rake test:railsredis'
sh 'bundle exec appraisal rails5-postgres-redis-activesupport rake test:railsredis'
sh 'bundle exec appraisal rails5-postgres-sidekiq rake test:railssidekiq'
sh 'bundle exec appraisal rails5-postgres-sidekiq rake test:railsactivejob'
sh 'bundle exec appraisal rails5-postgres rake test:railsdisableenv'
Expand Down
26 changes: 26 additions & 0 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ To contribute, check out the [contribution guidelines][contribution docs] and [d
- [Integration instrumentation](#integration-instrumentation)
- [Active Model Serializers](#active-model-serializers)
- [Active Record](#active-record)
- [Active Support](#active-support)
- [AWS](#aws)
- [Concurrent Ruby](#concurrent-ruby)
- [Dalli](#dalli)
Expand Down Expand Up @@ -322,6 +323,7 @@ For a list of available integrations, and their configuration options, please re
| ------------------------ | -------------------------- | ------------------------ | ----------------------------------- | ------------------------------------------------------------------------------ |
| Active Model Serializers | `active_model_serializers` | `>= 0.9` | *[Link](#active-model-serializers)* | *[Link](https://github.com/rails-api/active_model_serializers)* |
| Active Record | `active_record` | `>= 3.2, < 6.0` | *[Link](#active-record)* | *[Link](https://github.com/rails/rails/tree/master/activerecord)* |
| Active Support | `active_support` | `>= 3.2, < 6.0` | *[Link](#active-support)* | *[Link](https://github.com/rails/rails/tree/master/activesupport)* |
| AWS | `aws` | `>= 2.0` | *[Link](#aws)* | *[Link](https://github.com/aws/aws-sdk-ruby)* |
| Concurrent Ruby | `concurrent_ruby` | `>= 0.9` | *[Link](#concurrent-ruby)* | *[Link](https://github.com/ruby-concurrency/concurrent-ruby)* |
| Dalli | `dalli` | `>= 2.7` | *[Link](#dalli)* | *[Link](https://github.com/petergoldstein/dalli)* |
Expand Down Expand Up @@ -438,6 +440,30 @@ end

If ActiveRecord traces an event that uses a connection that matches a key defined by `describes`, it will use the trace settings assigned to that connection. If the connection does not match any of the described connections, it will use default settings defined by `c.use :active_record` instead.

### Active Support

Most of the time, Active Support is set up as part of Rails, but it can be activated separately:

```ruby
require 'activesupport'
require 'ddtrace'

Datadog.configure do |c|
c.use :active_support, options
end

cache = ActiveSupport::Cache::MemoryStore.new
cache.read('city')
```

Where `options` is an optional `Hash` that accepts the following parameters:

| Key | Description | Default |
| ---| --- | --- |
| `analytics_enabled` | Enable analytics for spans produced by this integration. `true` for on, `nil` to defer to global setting, `false` for off. | `false` |
| `cache_service` | Service name used for caching with `active_support` instrumentation. | `'active_support-cache'` |
| `tracer` | `Datadog::Tracer` used to perform instrumentation. Usually you don't need to set this. | `Datadog.tracer` |

### AWS

The AWS integration will trace every interaction (e.g. API calls) with AWS services (S3, ElastiCache etc.).
Expand Down
1 change: 1 addition & 0 deletions lib/ddtrace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module Datadog

require 'ddtrace/contrib/active_model_serializers/integration'
require 'ddtrace/contrib/active_record/integration'
require 'ddtrace/contrib/active_support/integration'
require 'ddtrace/contrib/aws/integration'
require 'ddtrace/contrib/concurrent_ruby/integration'
require 'ddtrace/contrib/dalli/integration'
Expand Down
157 changes: 157 additions & 0 deletions lib/ddtrace/contrib/active_support/cache/instrumentation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
require 'ddtrace/contrib/active_support/ext'

module Datadog
module Contrib
module ActiveSupport
module Cache
# Defines instrumentation for ActiveSupport caching
# rubocop:disable Lint/RescueException
module Instrumentation
module_function

def start_trace_cache(payload)
tracer = Datadog.configuration[:active_support][:tracer]

# In most of the cases Rails ``fetch()`` and ``read()`` calls are nested.
# This check ensures that two reads are not nested since they don't provide
# interesting details.
# NOTE: the ``finish_trace_cache()`` is fired but it already has a safe-guard
# to avoid any kind of issue.
current_span = tracer.active_span
return if payload[:action] == Ext::RESOURCE_CACHE_GET &&
current_span.try(:name) == Ext::SPAN_CACHE &&
current_span.try(:resource) == Ext::RESOURCE_CACHE_GET

tracing_context = payload.fetch(:tracing_context)

# create a new ``Span`` and add it to the tracing context
service = Datadog.configuration[:active_support][:cache_service]
type = Ext::SPAN_TYPE_CACHE
span = tracer.trace(Ext::SPAN_CACHE, service: service, span_type: type)
span.resource = payload.fetch(:action)
tracing_context[:dd_cache_span] = span
rescue StandardError => e
Datadog::Tracer.log.debug(e.message)
end

def finish_trace_cache(payload)
# retrieve the tracing context and continue the trace
tracing_context = payload.fetch(:tracing_context)
span = tracing_context[:dd_cache_span]
return unless span && !span.finished?

begin
# discard parameters from the cache_store configuration
if defined?(::Rails)
store, = *Array.wrap(::Rails.configuration.cache_store).flatten
span.set_tag(Ext::TAG_CACHE_BACKEND, store)
end

normalized_key = ::ActiveSupport::Cache.expand_cache_key(payload.fetch(:key))
cache_key = Datadog::Utils.truncate(normalized_key, Ext::QUANTIZE_CACHE_MAX_KEY_SIZE)
span.set_tag(Ext::TAG_CACHE_KEY, cache_key)

span.set_error(payload[:exception]) if payload[:exception]
ensure
span.finish
end
rescue StandardError => e
Datadog::Tracer.log.debug(e.message)
end

# Defines instrumentation for ActiveSupport cache reading
module Read
def read(*args, &block)
payload = {
action: Ext::RESOURCE_CACHE_GET,
key: args[0],
tracing_context: {}
}

begin
# process and catch cache exceptions
Instrumentation.start_trace_cache(payload)
super
rescue Exception => e
payload[:exception] = [e.class.name, e.message]
payload[:exception_object] = e
raise e
end
ensure
Instrumentation.finish_trace_cache(payload)
end
end

# Defines instrumentation for ActiveSupport cache fetching
module Fetch
def fetch(*args, &block)
payload = {
action: Ext::RESOURCE_CACHE_GET,
key: args[0],
tracing_context: {}
}

begin
# process and catch cache exceptions
Instrumentation.start_trace_cache(payload)
super
rescue Exception => e
payload[:exception] = [e.class.name, e.message]
payload[:exception_object] = e
raise e
end
ensure
Instrumentation.finish_trace_cache(payload)
end
end

# Defines instrumentation for ActiveSupport cache writing
module Write
def write(*args, &block)
payload = {
action: Ext::RESOURCE_CACHE_SET,
key: args[0],
tracing_context: {}
}

begin
# process and catch cache exceptions
Instrumentation.start_trace_cache(payload)
super
rescue Exception => e
payload[:exception] = [e.class.name, e.message]
payload[:exception_object] = e
raise e
end
ensure
Instrumentation.finish_trace_cache(payload)
end
end

# Defines instrumentation for ActiveSupport cache deleting
module Delete
def delete(*args, &block)
payload = {
action: Ext::RESOURCE_CACHE_DELETE,
key: args[0],
tracing_context: {}
}

begin
# process and catch cache exceptions
Instrumentation.start_trace_cache(payload)
super
rescue Exception => e
payload[:exception] = [e.class.name, e.message]
payload[:exception_object] = e
raise e
end
ensure
Instrumentation.finish_trace_cache(payload)
end
end
end
end
end
end
end
62 changes: 62 additions & 0 deletions lib/ddtrace/contrib/active_support/cache/patcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require 'ddtrace/contrib/patcher'
require 'ddtrace/contrib/active_support/cache/instrumentation'

module Datadog
module Contrib
module ActiveSupport
module Cache
# Patcher enables patching of 'active_support' module.
module Patcher
include Contrib::Patcher

module_function

def patched?
done?(:cache)
end

def patch
do_once(:cache) do
begin
patch_cache_store_read
patch_cache_store_fetch
patch_cache_store_write
patch_cache_store_delete
rescue StandardError => e
Datadog::Tracer.log.error("Unable to apply Active Support cache integration: #{e}")
end
end
end

def cache_store_class(meth)
::ActiveSupport::Cache::Store
end

def patch_cache_store_read
do_once(:patch_cache_store_read) do
cache_store_class(:read).send(:prepend, Cache::Instrumentation::Read)
end
end

def patch_cache_store_fetch
do_once(:patch_cache_store_fetch) do
cache_store_class(:fetch).send(:prepend, Cache::Instrumentation::Fetch)
end
end

def patch_cache_store_write
do_once(:patch_cache_store_write) do
cache_store_class(:write).send(:prepend, Cache::Instrumentation::Write)
end
end

def patch_cache_store_delete
do_once(:patch_cache_store_delete) do
cache_store_class(:delete).send(:prepend, Cache::Instrumentation::Delete)
end
end
end
end
end
end
end
47 changes: 47 additions & 0 deletions lib/ddtrace/contrib/active_support/cache/redis.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'ddtrace/contrib/active_support/cache/patcher'

module Datadog
module Contrib
module ActiveSupport
module Cache
# Support for Redis with ActiveSupport
module Redis
# Patching behavior for Redis with ActiveSupport
module Patcher
# For Rails < 5.2 w/ redis-activesupport...
# When Redis is used, we can't only patch Cache::Store as it is
# Cache::RedisStore, a sub-class of it that is used, in practice.
# We need to do a per-method monkey patching as some of them might
# be redefined, and some of them not. The latest version of redis-activesupport
# redefines write but leaves untouched read and delete:
# https://github.com/redis-store/redis-activesupport/blob/master/lib/active_support/cache/redis_store.rb
#
# For Rails >= 5.2 w/o redis-activesupport...
# ActiveSupport includes a Redis cache store internally, and does not require these overrides.
# https://github.com/rails/rails/blob/master/activesupport/lib/active_support/cache/redis_cache_store.rb
def patch_redis?(meth)
!Gem.loaded_specs['redis-activesupport'].nil? \
&& defined?(::ActiveSupport::Cache::RedisStore) \
&& ::ActiveSupport::Cache::RedisStore.instance_methods(false).include?(meth)
end

def cache_store_class(meth)
if patch_redis?(meth)
::ActiveSupport::Cache::RedisStore
else
super
end
end
end

# Decorate Cache patcher with Redis support
Cache::Patcher.instance_eval do
class << self
prepend Redis::Patcher
end
end
end
end
end
end
end
23 changes: 23 additions & 0 deletions lib/ddtrace/contrib/active_support/configuration/settings.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'ddtrace/contrib/configuration/settings'
require 'ddtrace/contrib/active_support/ext'

module Datadog
module Contrib
module ActiveSupport
module Configuration
# Custom settings for the ActiveSupport integration
class Settings < Contrib::Configuration::Settings
option :analytics_enabled,
default: -> { env_to_bool(Ext::ENV_ANALYTICS_ENABLED, false) },
lazy: true

option :analytics_sample_rate,
default: -> { env_to_float(Ext::ENV_ANALYTICS_SAMPLE_RATE, 1.0) },
lazy: true

option :cache_service, default: Ext::SERVICE_CACHE
end
end
end
end
end
Loading