-
Notifications
You must be signed in to change notification settings - Fork 598
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Active Support has notifications built in to instrument caching-related activities, including: * cache_read * cache_generate * cache_fetch_hit * cache_write * cache_delete * cache_exist? The payload always includes the key and the name of the store class. cache_read also includes whether the read is a hit, and :super_operation when a read is used with https://guides.rubyonrails.org/active_support_instrumentation.html#active-support When these operations are executed, a new segment will be started that includes the payload within the params attribute.
- Loading branch information
1 parent
c28a618
commit 4e3175e
Showing
8 changed files
with
351 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
lib/new_relic/agent/instrumentation/active_support_subscriber.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# This file is distributed under New Relic's license terms. | ||
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. | ||
# frozen_string_literal: true | ||
|
||
require 'new_relic/agent/instrumentation/notifications_subscriber' | ||
|
||
module NewRelic | ||
module Agent | ||
module Instrumentation | ||
class ActiveSupportSubscriber < NotificationsSubscriber | ||
def start(name, id, payload) | ||
return unless state.is_execution_traced? | ||
|
||
start_segment(name, id, payload) | ||
rescue => e | ||
log_notification_error(e, name, 'start') | ||
end | ||
|
||
def finish(name, id, payload) | ||
return unless state.is_execution_traced? | ||
|
||
finish_segment(id, payload) | ||
rescue => e | ||
log_notification_error(e, name, 'finish') | ||
end | ||
|
||
def start_segment(name, id, payload) | ||
segment = Tracer.start_segment(name: metric_name(name, payload)) | ||
|
||
add_segment_params(segment, payload) | ||
push_segment(id, segment) | ||
end | ||
|
||
def add_segment_params(segment, payload) | ||
segment.params[:key] = payload[:key] | ||
segment.params[:store] = payload[:store] | ||
segment.params[:hit] = payload[:hit] if payload.key?(:hit) | ||
segment.params[:super_operation] = payload[:super_operation] if payload.key?(:super_operation) | ||
segment | ||
end | ||
|
||
def finish_segment(id, payload) | ||
if segment = pop_segment(id) | ||
if exception = exception_object(payload) | ||
segment.notice_error(exception) | ||
end | ||
segment.finish | ||
end | ||
end | ||
|
||
def metric_name(name, payload) | ||
store = payload[:store] | ||
method = method_from_name(name) | ||
"Ruby/ActiveSupport/#{store}/#{method}" | ||
end | ||
|
||
PATTERN = /\Acache_([^\.]*)\.active_support\z/ | ||
UNKNOWN = "unknown".freeze | ||
|
||
METHOD_NAME_MAPPING = Hash.new do |h, k| | ||
if PATTERN =~ k | ||
h[k] = $1 | ||
else | ||
h[k] = UNKNOWN | ||
end | ||
end | ||
|
||
def method_from_name(name) | ||
METHOD_NAME_MAPPING[name] | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# This file is distributed under New Relic's license terms. | ||
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. | ||
# frozen_string_literal: true | ||
|
||
# This is a helper file that will allow apps using ActiveSupport without Rails | ||
# to still leverage all ActiveSupport based instrumentation functionality | ||
# offered by the agent that would otherwise be gated by the detection of Rails. | ||
|
||
# ActiveSupport notifications custom events | ||
if !defined?(Rails) && defined?(ActiveSupport::Notifications) && defined?(ActiveSupport::IsolatedExecutionState) | ||
require_relative 'rails_notifications/custom_events' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
test/new_relic/agent/instrumentation/active_support_subscriber_test.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# This file is distributed under New Relic's license terms. | ||
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. | ||
# frozen_string_literal: true | ||
|
||
require_relative '../../../test_helper' | ||
require 'new_relic/agent/instrumentation/active_support_subscriber' | ||
|
||
if defined?(ActiveSupport) | ||
require_relative 'rails/active_support_subscriber' | ||
else | ||
puts "Skipping tests in #{__FILE__} because Active Support is unavailable" | ||
end |
213 changes: 213 additions & 0 deletions
213
test/new_relic/agent/instrumentation/rails/active_support_subscriber.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
# This file is distributed under New Relic's license terms. | ||
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. | ||
# frozen_string_literal: true | ||
|
||
require_relative '../../../../test_helper' | ||
|
||
module NewRelic | ||
module Agent | ||
module Instrumentation | ||
class ActiveSupportSubscriberTest < Minitest::Test | ||
DEFAULT_STORE = 'MemCacheStore' | ||
METRIC_PREFIX = 'Ruby/ActiveSupport/' | ||
DEFAULT_PARAMS = {key: fake_guid(32), store: DEFAULT_STORE} | ||
DEFAULT_EVENT = 'cache_read.active_support' | ||
|
||
def setup | ||
nr_freeze_process_time | ||
@subscriber = ActiveSupportSubscriber.new | ||
@id = fake_guid(32) | ||
|
||
NewRelic::Agent.drop_buffered_data | ||
end | ||
|
||
def teardown | ||
NewRelic::Agent.drop_buffered_data | ||
end | ||
|
||
def test_start_when_not_traced | ||
@subscriber.state.stub :is_execution_traced?, false do | ||
in_transaction do |txn| | ||
@subscriber.start(DEFAULT_EVENT, @id, {}) | ||
|
||
assert_empty txn.segments | ||
end | ||
end | ||
end | ||
|
||
def test_finish_when_not_traced | ||
@subscriber.state.stub :is_execution_traced?, false do | ||
in_transaction do |txn| | ||
@subscriber.finish(DEFAULT_EVENT, @id, {}) | ||
|
||
assert_empty txn.segments | ||
end | ||
end | ||
end | ||
|
||
def test_metrics_recorded_for_known_methods | ||
method_name_mapping = { | ||
"cache_read.active_support" => "read".freeze, | ||
"cache_generate.active_support" => "generate".freeze, | ||
"cache_fetch_hit.active_support" => "fetch_hit".freeze, | ||
"cache_write.active_support" => "write".freeze, | ||
"cache_delete.active_support" => "delete".freeze, | ||
"cache_exist?.active_support" => "exist?".freeze | ||
} | ||
|
||
in_transaction('test') do | ||
method_name_mapping.keys.each do |event_name| | ||
generate_event(event_name) | ||
end | ||
end | ||
|
||
method_name_mapping.values.each do |method_name| | ||
assert_metrics_recorded "#{METRIC_PREFIX}#{DEFAULT_STORE}/#{method_name}" | ||
end | ||
end | ||
|
||
def test_metric_recorded_for_new_event_names | ||
in_transaction('test') do | ||
generate_event('cache_new_method.active_support') | ||
end | ||
|
||
assert_metrics_recorded "#{METRIC_PREFIX}#{DEFAULT_STORE}/new_method" | ||
end | ||
|
||
def test_failsafe_if_event_does_not_match_expected_pattern | ||
in_transaction('test') do | ||
generate_event('charcuterie_build_a_board_workshop') | ||
end | ||
|
||
assert_metrics_recorded "#{METRIC_PREFIX}#{DEFAULT_STORE}/unknown" | ||
end | ||
|
||
def test_key_recorded_as_attribute_on_traces | ||
key = 'blades' | ||
txn = in_transaction('test') do | ||
generate_event('cache_read.active_support', key: key, hit: false) | ||
end | ||
|
||
trace = last_transaction_trace | ||
tt_node = find_node_with_name(trace, "#{METRIC_PREFIX}#{DEFAULT_STORE}/read") | ||
|
||
assert_equal key, tt_node.params[:key] | ||
end | ||
|
||
def test_hit_recorded_as_attribute_on_traces | ||
txn = in_transaction('test') do | ||
generate_event('cache_read.active_support', DEFAULT_PARAMS.merge(hit: false)) | ||
end | ||
|
||
trace = last_transaction_trace | ||
tt_node = find_node_with_name(trace, "#{METRIC_PREFIX}#{DEFAULT_STORE}/read") | ||
|
||
puts txn.segments.last.inspect | ||
|
||
assert tt_node.params.key?(:hit) | ||
refute tt_node.params[:hit] | ||
end | ||
|
||
def test_super_operation_recorded_as_attribute_on_traces | ||
txn = in_transaction('test') do | ||
generate_event('cache_read.active_support', DEFAULT_PARAMS.merge(super_operation: nil)) | ||
end | ||
|
||
trace = last_transaction_trace | ||
tt_node = find_node_with_name(trace, "#{METRIC_PREFIX}#{DEFAULT_STORE}/read") | ||
|
||
puts txn.segments.last.inspect | ||
|
||
assert tt_node.params.key?(:super_operation) | ||
refute tt_node.params[:super_operation] | ||
end | ||
|
||
def test_segment_created | ||
in_transaction('test') do | ||
txn = NewRelic::Agent::Tracer.current_transaction | ||
|
||
assert_equal 1, txn.segments.size | ||
|
||
generate_event('cache_write.active_support', key: 'blade') | ||
|
||
assert_equal 2, txn.segments.size | ||
assert_equal "#{METRIC_PREFIX}#{DEFAULT_STORE}/write", txn.segments.last.name | ||
assert_predicate txn.segments.last, :finished?, "Segment #{txn.segments.last.name} was never finished. " | ||
end | ||
end | ||
|
||
def test_records_span_level_error | ||
exception_class = StandardError | ||
exception_msg = 'Natural 1' | ||
exception = exception_class.new(msg = exception_msg) | ||
# :exception_object was added in Rails 5 and above | ||
params = {:exception_object => exception, :exception => [exception_class.name, exception_msg]} | ||
|
||
txn = nil | ||
|
||
in_transaction do |test_txn| | ||
txn = test_txn | ||
generate_event('cache_fetch_hit.active_support', params) | ||
end | ||
|
||
assert_segment_noticed_error txn, /fetch/i, exception_class.name, /Natural 1/i | ||
end | ||
|
||
def test_pop_segment_returns_false | ||
@subscriber.stub :pop_segment, nil do | ||
txn = in_transaction do |txn| | ||
@subscriber.finish(DEFAULT_EVENT, @id, {}) | ||
end | ||
|
||
assert txn.segments.none? { |s| s.name.include?('ActiveSupport') } | ||
end | ||
end | ||
|
||
def test_start_logs_notification_error | ||
logger = MiniTest::Mock.new | ||
|
||
NewRelic::Agent.stub :logger, logger do | ||
logger.expect :error, nil, [/Error during .* callback/] | ||
logger.expect :log_exception, nil, [:error, ArgumentError] | ||
|
||
in_transaction do |txn| | ||
@subscriber.stub :start_segment, -> { raise 'kaboom' } do | ||
@subscriber.start(DEFAULT_EVENT, @id, {}) | ||
end | ||
|
||
assert_equal 1, txn.segments.size | ||
end | ||
end | ||
logger.verify | ||
end | ||
|
||
def test_finish_logs_notification_error | ||
logger = MiniTest::Mock.new | ||
|
||
NewRelic::Agent.stub :logger, logger do | ||
logger.expect :error, nil, [/Error during .* callback/] | ||
logger.expect :log_exception, nil, [:error, ArgumentError] | ||
|
||
in_transaction do |txn| | ||
@subscriber.stub :finish_segment, -> { raise 'kaboom' } do | ||
@subscriber.finish(DEFAULT_EVENT, @id, {}) | ||
end | ||
|
||
assert_equal 1, txn.segments.size | ||
end | ||
end | ||
logger.verify | ||
end | ||
|
||
private | ||
|
||
def generate_event(event_name, attributes = {}) | ||
payload = DEFAULT_PARAMS.merge(attributes) | ||
@subscriber.start(event_name, @id, payload) | ||
yield if block_given? | ||
@subscriber.finish(event_name, @id, payload) | ||
end | ||
end | ||
end | ||
end | ||
end |