-
Notifications
You must be signed in to change notification settings - Fork 600
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
Add async http instrumentation #2272
Changes from all commits
0587815
37e3044
2219100
76915eb
d24d89a
740bd51
8d0b0cb
ca6da27
0b3edb6
00f0fce
33333f6
6be1dc1
fef6ba7
6717a9f
1906c10
f148bfd
bad57ad
3285c5b
e911c78
57c81fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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 | ||
tannalynn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else | ||
chain_instrument NewRelic::Agent::Instrumentation::AsyncHttp::Chain | ||
end | ||
end | ||
end |
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 |
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 | ||
tannalynn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -382,7 +382,11 @@ common: &default_settings | |
# prepend, chain, disabled. | ||
# instrumentation.bunny: auto | ||
|
||
# Controls auto-instrumentation of the concurrent-ruby library at start-up. May be | ||
# Controls auto-instrumentation of Async::HTTP at start up. | ||
# May be one of [auto|prepend|chain|disabled] | ||
# instrumentation.async_http: auto | ||
|
||
# Controls auto-instrumentation of the concurrent-ruby library at start up. May be | ||
Comment on lines
+385
to
+389
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tannalynn - Out of curiosity, did you add this manually or use the script? (If you used the script, I'm a bit concerned about why the first version was outdented one level) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used the generator originally, but there was a merge conflict so i had to copy it and put it back, it's very possible that i messed up the indentation when i resolved the merge conflict. |
||
# one of: auto, prepend, chain, disabled. | ||
# instrumentation.concurrent_ruby: auto | ||
|
||
|
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) |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for this PR, but maybe it's time to move these constants to a shared location.