-
Notifications
You must be signed in to change notification settings - Fork 602
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
Ethon instrumentation #2260
Merged
Ethon instrumentation #2260
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
0eb7e31
Ethon instrumentation
fallwith e361374
Ethon config option
fallwith 7ee32db
CI: add 'ethon' to http clients 2 list
fallwith ba66c2e
Ethon: permit 2 more tests to run
fallwith 10ab87b
Ethon: improved headers and errors handling
fallwith c7343ae
Ethon instrumentation updates
fallwith df37071
Ethon: handle response headers properly
fallwith d81f14c
instrumentation for Ethon::Multi
fallwith dc87b1a
Merge branch 'dev' into ethon
fallwith 4194e08
CHANGELOG entry for Ethon
fallwith 8ce82aa
Typhoeus + Ethon fixups
fallwith 7eeee3f
Ethon: focus on return_code for errors
fallwith 30dc1fe
Ethon instrumentation: default action, naming
fallwith 55b25da
Ethon: test request wrapper host determination
fallwith b2481d0
Merge pull request #2274 from newrelic/ethon_with_typhoeus
fallwith bbf0a2f
Ethon: disable when Typhoues is present
fallwith 914302d
Test cases: stop special casing Typhoeus + Ethon
fallwith 40b5fd2
Merge branch 'dev' into ethon
fallwith 285de8a
Ethon: PR feedback
fallwith File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,111 @@ | ||
# 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 'uri' | ||
require_relative 'abstract' | ||
|
||
module NewRelic | ||
module Agent | ||
module HTTPClients | ||
class EthonHTTPResponse < AbstractResponse | ||
def initialize(easy) | ||
@easy = easy | ||
end | ||
|
||
def status_code | ||
@easy.response_code | ||
end | ||
|
||
def [](key) | ||
headers[format_key(key)] | ||
end | ||
|
||
def headers | ||
# Ethon::Easy#response_headers will return '' if headers are unset | ||
@easy.response_headers.scan(/\n([^:]+?): ([^:\n]+?)\r/).each_with_object({}) do |pair, hash| | ||
hash[format_key(pair[0])] = pair[1] | ||
end | ||
end | ||
alias to_hash headers | ||
|
||
private | ||
|
||
def format_key(key) | ||
key.tr('-', '_').downcase | ||
end | ||
end | ||
|
||
class EthonHTTPRequest < AbstractRequest | ||
attr_reader :uri | ||
|
||
DEFAULT_ACTION = 'GET' | ||
DEFAULT_HOST = 'UNKNOWN_HOST' | ||
ETHON = 'Ethon' | ||
LHOST = 'host'.freeze | ||
UHOST = 'Host'.freeze | ||
|
||
def initialize(easy) | ||
@easy = easy | ||
@uri = uri_from_easy | ||
end | ||
|
||
def type | ||
ETHON | ||
end | ||
|
||
def host_from_header | ||
self[LHOST] || self[UHOST] | ||
end | ||
|
||
def uri_from_easy | ||
# anticipate `Ethon::Easy#url` being `example.com` without a protocol | ||
# defined and use an 'http' protocol prefix for `URI.parse` to work | ||
# with the URL as desired | ||
url_str = @easy.url.match?(':') ? @easy.url : "http://#{@easy.url}" | ||
begin | ||
URI.parse(url_str) | ||
rescue URI::InvalidURIError => e | ||
NewRelic::Agent.logger.debug("Failed to parse URI '#{url_str}': #{e.class} - #{e.message}") | ||
URI.parse(NewRelic::EMPTY_STR) | ||
end | ||
end | ||
|
||
def host | ||
host_from_header || uri.host&.downcase || DEFAULT_HOST | ||
end | ||
|
||
def method | ||
return DEFAULT_ACTION unless @easy.instance_variable_defined?(action_instance_var) | ||
|
||
@easy.instance_variable_get(action_instance_var) | ||
end | ||
|
||
def action_instance_var | ||
NewRelic::Agent::Instrumentation::Ethon::Easy::ACTION_INSTANCE_VAR | ||
end | ||
|
||
def []=(key, value) | ||
headers[key] = value | ||
@easy.headers = headers | ||
end | ||
|
||
def headers | ||
@headers ||= if @easy.instance_variable_defined?(headers_instance_var) | ||
@easy.instance_variable_get(headers_instance_var) | ||
else | ||
{} | ||
end | ||
end | ||
|
||
def headers_instance_var | ||
NewRelic::Agent::Instrumentation::Ethon::Easy::HEADERS_INSTANCE_VAR | ||
end | ||
|
||
def [](key) | ||
headers[key] | ||
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,30 @@ | ||
# 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 'ethon/instrumentation' | ||
require_relative 'ethon/chain' | ||
require_relative 'ethon/prepend' | ||
|
||
DependencyDetection.defer do | ||
named :ethon | ||
|
||
depends_on do | ||
defined?(Ethon) && Gem::Version.new(Ethon::VERSION) >= Gem::Version.new('0.12.0') | ||
end | ||
|
||
executes do | ||
NewRelic::Agent.logger.info('Installing ethon instrumentation') | ||
end | ||
|
||
executes do | ||
if use_prepend? | ||
# NOTE: to prevent a string like 'Ethon::Easy' from being converted into | ||
# 'Ethon/Easy', a 3rd argument is supplied to `prepend_instrument` | ||
fallwith marked this conversation as resolved.
Show resolved
Hide resolved
|
||
prepend_instrument Ethon::Easy, NewRelic::Agent::Instrumentation::Ethon::Easy::Prepend, Ethon::Easy.name | ||
prepend_instrument Ethon::Multi, NewRelic::Agent::Instrumentation::Ethon::Multi::Prepend, Ethon::Multi.name | ||
else | ||
chain_instrument NewRelic::Agent::Instrumentation::Ethon::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,39 @@ | ||
# 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 Ethon | ||
module Chain | ||
def self.instrument! | ||
::Ethon::Easy.class_eval do | ||
include NewRelic::Agent::Instrumentation::Ethon::Easy | ||
|
||
alias_method(:fabricate_without_tracing, :fabricate) | ||
def fabricate(url, action_name, options) | ||
fabricate_with_tracing(url, action_name, options) { fabricate_without_tracing(url, action_name, options) } | ||
end | ||
|
||
alias_method(:headers_equals_without_tracing, :headers=) | ||
def headers=(headers) | ||
headers_equals_with_tracing(headers) { headers_equals_without_tracing(headers) } | ||
end | ||
|
||
alias_method(:perform_without_tracing, :perform) | ||
def perform(*args) | ||
perform_with_tracing(*args) { perform_without_tracing(*args) } | ||
end | ||
end | ||
|
||
::Ethon::Multi.class_eval do | ||
include NewRelic::Agent::Instrumentation::Ethon::Multi | ||
|
||
alias_method(:perform_without_tracing, :perform) | ||
def perform(*args) | ||
perform_with_tracing(*args) { perform_without_tracing(*args) } | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
105 changes: 105 additions & 0 deletions
105
lib/new_relic/agent/instrumentation/ethon/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,105 @@ | ||
# 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/ethon_wrappers' | ||
|
||
module NewRelic::Agent::Instrumentation | ||
module Ethon | ||
module NRShared | ||
INSTRUMENTATION_NAME = 'Ethon' | ||
NOTICEABLE_ERROR_CLASS = 'Ethon::Errors::EthonError' | ||
|
||
def prep_easy(easy, parent = nil) | ||
wrapped_request = NewRelic::Agent::HTTPClients::EthonHTTPRequest.new(easy) | ||
segment = NewRelic::Agent::Tracer.start_external_request_segment( | ||
library: wrapped_request.type, | ||
uri: wrapped_request.uri, | ||
procedure: wrapped_request.method, | ||
parent: parent | ||
) | ||
segment.add_request_headers(wrapped_request) | ||
|
||
callback = proc do | ||
wrapped_response = NewRelic::Agent::HTTPClients::EthonHTTPResponse.new(easy) | ||
segment.process_response_headers(wrapped_response) | ||
|
||
if easy.return_code != :ok | ||
e = NewRelic::Agent::NoticeableError.new(NOTICEABLE_ERROR_CLASS, | ||
"return_code: >>#{easy.return_code}<<, response_code: >>#{easy.response_code}<<") | ||
segment.notice_error(e) | ||
end | ||
|
||
::NewRelic::Agent::Transaction::Segment.finish(segment) | ||
end | ||
|
||
easy.on_complete { callback.call } | ||
|
||
segment | ||
end | ||
|
||
def wrap_with_tracing(segment, &block) | ||
NewRelic::Agent.record_instrumentation_invocation(INSTRUMENTATION_NAME) | ||
|
||
NewRelic::Agent::Tracer.capture_segment_error(segment) do | ||
yield | ||
end | ||
ensure | ||
NewRelic::Agent::Transaction::Segment.finish(segment) | ||
end | ||
end | ||
|
||
module Easy | ||
include NRShared | ||
|
||
ACTION_INSTANCE_VAR = :@nr_action | ||
HEADERS_INSTANCE_VAR = :@nr_headers | ||
|
||
# `Ethon::Easy` doesn't expose the "action name" ('GET', 'POST', etc.) | ||
# and Ethon's fabrication of HTTP classes uses | ||
# `Ethon::Easy::Http::Custom` for non-standard actions. To be able to | ||
# know the action name at `#perform` time, we set a new instance variable | ||
# on the `Ethon::Easy` instance with the base name of the fabricated | ||
# class, respecting the 'Custom' name where appropriate. | ||
def fabricate_with_tracing(_url, action_name, _options) | ||
fabbed = yield | ||
instance_variable_set(ACTION_INSTANCE_VAR, NewRelic::Agent.base_name(fabbed.class.name).upcase) | ||
fabbed | ||
end | ||
|
||
# `Ethon::Easy` uses `Ethon::Easy::Header` to set request headers on | ||
# libcurl with `#headers=`. After they are set, they aren't easy to get | ||
# at again except via FFI so set a new instance variable on the | ||
# `Ethon::Easy` instance to store them in Ruby hash format. | ||
def headers_equals_with_tracing(headers) | ||
instance_variable_set(HEADERS_INSTANCE_VAR, headers) | ||
yield | ||
end | ||
|
||
def perform_with_tracing(*args) | ||
return unless NewRelic::Agent::Tracer.state.is_execution_traced? | ||
|
||
segment = prep_easy(self) | ||
wrap_with_tracing(segment) { yield } | ||
end | ||
end | ||
|
||
module Multi | ||
include NRShared | ||
|
||
MULTI_SEGMENT_NAME = 'External/Multiple/Ethon::Multi/perform' | ||
|
||
def perform_with_tracing(*args) | ||
return unless NewRelic::Agent::Tracer.state.is_execution_traced? | ||
|
||
segment = NewRelic::Agent::Tracer.start_segment(name: MULTI_SEGMENT_NAME) | ||
|
||
wrap_with_tracing(segment) do | ||
easy_handles.each { |easy| prep_easy(easy, segment) } | ||
|
||
yield | ||
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,35 @@ | ||
# 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 Ethon | ||
module Easy | ||
module Prepend | ||
include NewRelic::Agent::Instrumentation::Ethon::Easy | ||
|
||
def fabricate(url, action_name, options) | ||
fabricate_with_tracing(url, action_name, options) { super } | ||
end | ||
|
||
def headers=(headers) | ||
headers_equals_with_tracing(headers) { super } | ||
end | ||
|
||
def perform(*args) | ||
perform_with_tracing(*args) { super } | ||
end | ||
end | ||
end | ||
|
||
module Multi | ||
module Prepend | ||
include NewRelic::Agent::Instrumentation::Ethon::Multi | ||
|
||
def perform(*args) | ||
perform_with_tracing(*args) { super } | ||
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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
nice comment I didn't know
prepend_instrument
supported a 3rd arg