-
Notifications
You must be signed in to change notification settings - Fork 600
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 #2272 from newrelic/add_async_http_instrumentation
Add async http instrumentation
- Loading branch information
Showing
12 changed files
with
373 additions
and
3 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,83 @@ | ||
# 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 'abstract' | ||
require 'resolv' | ||
|
||
module NewRelic | ||
module Agent | ||
module HTTPClients | ||
class AsyncHTTPResponse < AbstractResponse | ||
def get_status_code | ||
get_status_code_using(:status) | ||
end | ||
|
||
def [](key) | ||
to_hash[key.downcase]&.first | ||
end | ||
|
||
def to_hash | ||
@wrapped_response.headers.to_h | ||
end | ||
end | ||
|
||
class AsyncHTTPRequest < AbstractRequest | ||
def initialize(connection, method, url, headers) | ||
@connection = connection | ||
@method = method | ||
@url = ::NewRelic::Agent::HTTPClients::URIUtil.parse_and_normalize_url(url) | ||
@headers = headers | ||
end | ||
|
||
ASYNC_HTTP = 'Async::HTTP' | ||
LHOST = 'host' | ||
UHOST = 'Host' | ||
COLON = ':' | ||
|
||
def type | ||
ASYNC_HTTP | ||
end | ||
|
||
def host_from_header | ||
if hostname = (self[LHOST] || self[UHOST]) | ||
hostname.split(COLON).first | ||
end | ||
end | ||
|
||
def host | ||
host_from_header || uri.host.to_s | ||
end | ||
|
||
def [](key) | ||
return headers[key] unless headers.is_a?(Array) | ||
|
||
headers.each do |header| | ||
return header[1] if header[0].casecmp?(key) | ||
end | ||
nil | ||
end | ||
|
||
def []=(key, value) | ||
if headers.is_a?(Array) | ||
headers << [key, value] | ||
else | ||
headers[key] = value | ||
end | ||
end | ||
|
||
def uri | ||
@url | ||
end | ||
|
||
def headers | ||
@headers | ||
end | ||
|
||
def method | ||
@method | ||
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,26 @@ | ||
# 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 'async_http/instrumentation' | ||
require_relative 'async_http/chain' | ||
require_relative 'async_http/prepend' | ||
|
||
DependencyDetection.defer do | ||
named :async_http | ||
|
||
depends_on do | ||
defined?(Async::HTTP) && Gem::Version.new(Async::HTTP::VERSION) >= Gem::Version.new('0.59.0') | ||
end | ||
|
||
executes do | ||
NewRelic::Agent.logger.info('Installing async_http instrumentation') | ||
|
||
require 'async/http/internet' | ||
if use_prepend? | ||
prepend_instrument Async::HTTP::Internet, NewRelic::Agent::Instrumentation::AsyncHttp::Prepend | ||
else | ||
chain_instrument NewRelic::Agent::Instrumentation::AsyncHttp::Chain | ||
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,23 @@ | ||
# 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 'instrumentation' | ||
|
||
module NewRelic::Agent::Instrumentation | ||
module AsyncHttp::Chain | ||
def self.instrument! | ||
::Async::HTTP::Internet.class_eval do | ||
include NewRelic::Agent::Instrumentation::AsyncHttp | ||
|
||
alias_method(:call_without_new_relic, :call) | ||
|
||
def call(method, url, headers = nil, body = nil) | ||
call_with_new_relic(method, url, headers, body) do |hdr| | ||
call_without_new_relic(method, url, hdr, body) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
37 changes: 37 additions & 0 deletions
37
lib/new_relic/agent/instrumentation/async_http/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,37 @@ | ||
# 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/http_clients/async_http_wrappers' | ||
|
||
module NewRelic::Agent::Instrumentation | ||
module AsyncHttp | ||
def call_with_new_relic(method, url, headers = nil, body = nil) | ||
headers ||= {} # if it is nil, we need to make it a hash so we can insert headers | ||
wrapped_request = NewRelic::Agent::HTTPClients::AsyncHTTPRequest.new(self, method, url, headers) | ||
|
||
segment = NewRelic::Agent::Tracer.start_external_request_segment( | ||
library: wrapped_request.type, | ||
uri: wrapped_request.uri, | ||
procedure: wrapped_request.method | ||
) | ||
|
||
begin | ||
response = nil | ||
segment.add_request_headers(wrapped_request) | ||
|
||
NewRelic::Agent.disable_all_tracing do | ||
response = NewRelic::Agent::Tracer.capture_segment_error(segment) do | ||
yield(headers) | ||
end | ||
end | ||
|
||
wrapped_response = NewRelic::Agent::HTTPClients::AsyncHTTPResponse.new(response) | ||
segment.process_response_headers(wrapped_response) | ||
response | ||
ensure | ||
segment&.finish | ||
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,15 @@ | ||
# 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 'instrumentation' | ||
|
||
module NewRelic::Agent::Instrumentation | ||
module AsyncHttp::Prepend | ||
include NewRelic::Agent::Instrumentation::AsyncHttp | ||
|
||
def call(method, url, headers = nil, body = nil) | ||
call_with_new_relic(method, url, headers, body) { |hdr| super(method, url, hdr, body) } | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# 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 | ||
|
||
ASYNC_HTTP_VERSIONS = [ | ||
[nil, 2.5], | ||
['0.59.0', 2.5] | ||
] | ||
|
||
def gem_list(async_http_version = nil) | ||
<<~GEM_LIST | ||
gem 'async-http'#{async_http_version} | ||
gem 'rack' | ||
GEM_LIST | ||
end | ||
|
||
create_gemfiles(ASYNC_HTTP_VERSIONS) |
131 changes: 131 additions & 0 deletions
131
test/multiverse/suites/async_http/async_http_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,131 @@ | ||
# 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 'http_client_test_cases' | ||
|
||
class AsyncHttpInstrumentationTest < Minitest::Test | ||
include HttpClientTestCases | ||
|
||
def client_name | ||
'Async::HTTP' | ||
end | ||
|
||
def timeout_error_class | ||
Async::TimeoutError | ||
end | ||
|
||
def simulate_error_response | ||
Async::HTTP::Client.any_instance.stubs(:call).raises(timeout_error_class.new('read timeout reached')) | ||
get_response | ||
end | ||
|
||
def get_response(url = nil, headers = nil) | ||
request_and_wait(:get, url || default_url, headers) | ||
end | ||
|
||
def request_and_wait(method, url, headers = nil, body = nil) | ||
resp = nil | ||
Async do | ||
begin | ||
internet = Async::HTTP::Internet.new | ||
resp = internet.send(method, url, headers) | ||
@read_resp = resp&.read | ||
ensure | ||
internet&.close | ||
end | ||
end | ||
resp | ||
end | ||
|
||
def get_wrapped_response(url) | ||
NewRelic::Agent::HTTPClients::AsyncHTTPResponse.new(get_response(url)) | ||
end | ||
|
||
def head_response | ||
request_and_wait(:head, default_url) | ||
end | ||
|
||
def post_response | ||
request_and_wait(:post, default_url, nil, '') | ||
end | ||
|
||
def put_response | ||
request_and_wait(:put, default_url, nil, '') | ||
end | ||
|
||
def delete_response | ||
request_and_wait(:delete, default_url, nil, '') | ||
end | ||
|
||
def request_instance | ||
NewRelic::Agent::HTTPClients::AsyncHTTPRequest.new(Async::HTTP::Internet.new, 'GET', default_url, {}) | ||
end | ||
|
||
def response_instance(headers = {}) | ||
resp = get_response(default_url, headers) | ||
headers.each do |k, v| | ||
resp.headers[k] = v | ||
end | ||
|
||
NewRelic::Agent::HTTPClients::AsyncHTTPResponse.new(resp) | ||
end | ||
|
||
def body(res) | ||
@read_resp | ||
end | ||
|
||
def test_noticed_error_at_segment_and_txn_on_error | ||
# skipping this test | ||
# Async gem does not allow the errors to escape the async block | ||
# so the errors will never end up on the transaction, only ever the async http segment | ||
end | ||
|
||
def test_raw_synthetics_header_is_passed_along_if_present_array | ||
with_config(:"cross_application_tracer.enabled" => true) do | ||
in_transaction do | ||
NewRelic::Agent::Tracer.current_transaction.raw_synthetics_header = 'boo' | ||
|
||
get_response(default_url, [%w[itsaheader itsavalue]]) | ||
|
||
assert_equal 'boo', server.requests.last['HTTP_X_NEWRELIC_SYNTHETICS'] | ||
end | ||
end | ||
end | ||
|
||
def test_raw_synthetics_header_is_passed_along_if_present_hash | ||
with_config(:"cross_application_tracer.enabled" => true) do | ||
in_transaction do | ||
NewRelic::Agent::Tracer.current_transaction.raw_synthetics_header = 'boo' | ||
|
||
get_response(default_url, {'itsaheader' => 'itsavalue'}) | ||
|
||
assert_equal 'boo', server.requests.last['HTTP_X_NEWRELIC_SYNTHETICS'] | ||
end | ||
end | ||
end | ||
|
||
def test_raw_synthetics_header_is_passed_along_if_present_protocol_header_hash | ||
with_config(:"cross_application_tracer.enabled" => true) do | ||
in_transaction do | ||
NewRelic::Agent::Tracer.current_transaction.raw_synthetics_header = 'boo' | ||
|
||
get_response(default_url, ::Protocol::HTTP::Headers[{'itsaheader' => 'itsavalue'}]) | ||
|
||
assert_equal 'boo', server.requests.last['HTTP_X_NEWRELIC_SYNTHETICS'] | ||
end | ||
end | ||
end | ||
|
||
def test_raw_synthetics_header_is_passed_along_if_present_protocol_header_array | ||
with_config(:"cross_application_tracer.enabled" => true) do | ||
in_transaction do | ||
NewRelic::Agent::Tracer.current_transaction.raw_synthetics_header = 'boo' | ||
|
||
get_response(default_url, ::Protocol::HTTP::Headers[%w[itsaheader itsavalue]]) | ||
|
||
assert_equal 'boo', server.requests.last['HTTP_X_NEWRELIC_SYNTHETICS'] | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.