-
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.
Merge pull request #1682 from newrelic/concurrent_ruby_promises
Add instrumentation for Concurrent Ruby
- Loading branch information
Showing
16 changed files
with
348 additions
and
7 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# 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 'concurrent_ruby/instrumentation' | ||
require_relative 'concurrent_ruby/chain' | ||
require_relative 'concurrent_ruby/prepend' | ||
|
||
DependencyDetection.defer do | ||
named :'concurrent_ruby' | ||
|
||
depends_on do | ||
defined?(Concurrent) && | ||
Gem::Version.new(Concurrent::VERSION) >= Gem::Version.new('1.1.5') | ||
end | ||
|
||
executes do | ||
NewRelic::Agent.logger.info('Installing concurrent-ruby instrumentation') | ||
|
||
if use_prepend? | ||
prepend_instrument(Concurrent::ThreadPoolExecutor, NewRelic::Agent::Instrumentation::ConcurrentRuby::Prepend) | ||
|
||
[Concurrent::Promises.const_get(:'InternalStates')::Rejected, | ||
Concurrent::Promises.const_get(:'InternalStates')::PartiallyRejected].each do |klass| | ||
klass.prepend(NewRelic::Agent::Instrumentation::ConcurrentRuby::ErrorPrepend) | ||
end | ||
else | ||
chain_instrument NewRelic::Agent::Instrumentation::ConcurrentRuby::Chain | ||
end | ||
end | ||
end |
36 changes: 36 additions & 0 deletions
36
lib/new_relic/agent/instrumentation/concurrent_ruby/chain.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,36 @@ | ||
# 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 | ||
|
||
module NewRelic::Agent::Instrumentation | ||
module ConcurrentRuby::Chain | ||
def self.instrument! | ||
::Concurrent::ThreadPoolExecutor.class_eval do | ||
include NewRelic::Agent::Instrumentation::ConcurrentRuby | ||
|
||
alias_method(:post_without_new_relic, :post) | ||
|
||
def post(*args, &task) | ||
return post_without_new_relic(*args, &task) unless NewRelic::Agent::Tracer.tracing_enabled? | ||
|
||
traced_task = add_task_tracing(*args, &task) | ||
post_without_new_relic(*args, &traced_task) | ||
end | ||
end | ||
|
||
[::Concurrent::Promises.const_get(:'InternalStates')::Rejected, | ||
::Concurrent::Promises.const_get(:'InternalStates')::PartiallyRejected].each do |klass| | ||
klass.class_eval do | ||
alias_method(:initialize_without_new_relic, :initialize) | ||
|
||
# Uses args.last to record the error becuase the methods that this will monkey patch | ||
# look like: initialize(reason) & initialize(value, reason) | ||
def initialize(*args) | ||
NewRelic::Agent.notice_error(args.last) if args.last.is_a?(Exception) | ||
initialize_without_new_relic(*args) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
21 changes: 21 additions & 0 deletions
21
lib/new_relic/agent/instrumentation/concurrent_ruby/instrumentation.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,21 @@ | ||
# 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 | ||
|
||
module NewRelic::Agent::Instrumentation | ||
module ConcurrentRuby | ||
SEGMENT_NAME = 'Concurrent/Task' | ||
SUPPORTABILITY_METRIC = 'Supportability/ConcurrentRuby/Invoked' | ||
|
||
def add_task_tracing(*args, &task) | ||
NewRelic::Agent.record_metric_once(SUPPORTABILITY_METRIC) | ||
|
||
NewRelic::Agent::Tracer.thread_block_with_current_transaction( | ||
*args, | ||
segment_name: SEGMENT_NAME, | ||
parent: NewRelic::Agent::Tracer.current_segment, | ||
&task | ||
) | ||
end | ||
end | ||
end |
27 changes: 27 additions & 0 deletions
27
lib/new_relic/agent/instrumentation/concurrent_ruby/prepend.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,27 @@ | ||
# 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 | ||
|
||
module NewRelic::Agent::Instrumentation | ||
module ConcurrentRuby | ||
module Prepend | ||
include NewRelic::Agent::Instrumentation::ConcurrentRuby | ||
|
||
def post(*args, &task) | ||
return super(*args, &task) unless NewRelic::Agent::Tracer.tracing_enabled? | ||
|
||
traced_task = add_task_tracing(*args, &task) | ||
super(*args, &traced_task) | ||
end | ||
end | ||
|
||
module ErrorPrepend | ||
# Uses args.last to record the error because the methods that this will be prepended to | ||
# look like: initialize(reason) & initialize(value, reason) | ||
def initialize(*args) | ||
NewRelic::Agent.notice_error(args.last) if args.last.is_a?(Exception) | ||
super | ||
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# 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 | ||
|
||
instrumentation_methods :chain, :prepend | ||
|
||
# The 1.1.x series of Concurrent Ruby starts to use the error classes | ||
# we reference in our logic to notice errors after 1.1.5 | ||
# 1.1.4 and below do not use these classes. | ||
CONCURRENT_RUBY_VERSIONS = [ | ||
[nil, 2.2], | ||
['1.1.5', 2.2] | ||
] | ||
|
||
def gem_list(concurrent_version = nil) | ||
<<-RB | ||
gem 'concurrent-ruby'#{concurrent_version} | ||
RB | ||
end | ||
|
||
create_gemfiles(CONCURRENT_RUBY_VERSIONS, gem_list) |
121 changes: 121 additions & 0 deletions
121
test/multiverse/suites/concurrent_ruby/concurrent_ruby_instrumentation_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,121 @@ | ||
# 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 | ||
|
||
class ConcurrentRubyInstrumentationTest < Minitest::Test | ||
EXPECTED_SEGMENTS_FOR_NESTED_CALLS = [ | ||
'Concurrent/Task', | ||
'External/www.example.com/Net::HTTP/GET' | ||
] | ||
|
||
# Helper methods | ||
def future_in_transaction(&block) | ||
in_transaction do | ||
future = Concurrent::Promises.future { yield } | ||
future.wait! | ||
end | ||
end | ||
|
||
def concurrent_promises_calls_net_http_in_block | ||
future_in_transaction { Net::HTTP.get(URI('http://www.example.com')) } | ||
end | ||
|
||
def simulate_error | ||
future = Concurrent::Promises.future { raise 'hi' } | ||
future.wait! | ||
end | ||
|
||
def assert_segment_noticed_simulated_error(txn) | ||
assert_segment_noticed_error txn, /Concurrent\/Task$/, /RuntimeError/, /hi/i | ||
end | ||
|
||
def assert_expected_segments_in_transaction(txn) | ||
assert_predicate (txn.segments.map(&:name) & EXPECTED_SEGMENTS_FOR_NESTED_CALLS), :any? | ||
end | ||
|
||
# Tests | ||
def test_promises_future_creates_segment_with_default_name | ||
txn = future_in_transaction { 'time keeps on slipping' } | ||
expected_segment = 'Concurrent/Task' | ||
|
||
assert_equal(2, txn.segments.length) | ||
assert_includes txn.segments.map(&:name), expected_segment | ||
end | ||
|
||
def test_promises_future_creates_segments_for_nested_instrumented_calls | ||
with_config(:'instrumentation.thread.tracing' => false) do | ||
txn = concurrent_promises_calls_net_http_in_block | ||
|
||
assert_equal(3, txn.segments.length) | ||
assert_expected_segments_in_transaction(txn) | ||
end | ||
end | ||
|
||
def test_promises_future_creates_segments_for_nested_instrumented_calls_with_thread_tracing_enabled | ||
with_config(:'instrumentation.thread.tracing' => true) do | ||
txn = concurrent_promises_calls_net_http_in_block | ||
|
||
# We can't check the number of segments when thread tracing is enabled because we cannot rely on concurrent-ruby | ||
# creating threads during this transaction, as it can reuse threads that were created previously. | ||
# Instead, we check to make sure the segments that should be present are. | ||
assert_expected_segments_in_transaction(txn) | ||
end | ||
end | ||
|
||
def test_promises_future_captures_segment_error | ||
txn = in_transaction do | ||
# TODO: OLD RUBIES - RUBY_VERSION 2.2 | ||
# specific "begin" in block can be removed once we drop support for 2.2 | ||
begin | ||
simulate_error | ||
rescue StandardError => e | ||
# NOOP -- allowing span to notice error | ||
end | ||
end | ||
|
||
assert_segment_noticed_simulated_error(txn) | ||
end | ||
|
||
def test_noticed_error_at_segment_and_txn_on_error | ||
txn = nil | ||
begin | ||
in_transaction do |test_txn| | ||
txn = test_txn | ||
simulate_error | ||
end | ||
rescue StandardError => e | ||
# NOOP -- allowing span and transaction to notice error | ||
end | ||
|
||
assert_segment_noticed_simulated_error(txn) | ||
assert_transaction_noticed_error txn, /RuntimeError/ | ||
end | ||
|
||
def test_task_segment_has_correct_parent | ||
txn = future_in_transaction { 'are you my mother?' } | ||
task_segment = txn.segments.find { |n| n.name == 'Concurrent/Task' } | ||
|
||
assert_equal task_segment.parent.name, txn.best_name | ||
end | ||
|
||
def test_segment_not_created_if_tracing_disabled | ||
NewRelic::Agent::Tracer.stub :tracing_enabled?, false do | ||
txn = future_in_transaction { 'the revolution will not be televised' } | ||
|
||
assert_predicate txn.segments, :one? | ||
assert_equal txn.segments.first.name, txn.best_name | ||
end | ||
end | ||
|
||
def test_supportability_metric_recorded_once | ||
in_transaction do | ||
Concurrent::Promises.future { 'one-banana' } | ||
end | ||
|
||
in_transaction do | ||
Concurrent::Promises.future { 'two-banana' } | ||
end | ||
|
||
assert_metrics_recorded(NewRelic::Agent::Instrumentation::ConcurrentRuby::SUPPORTABILITY_METRIC) | ||
end | ||
end |
Oops, something went wrong.