Skip to content

Commit

Permalink
Merge branch 'dev' into ethon
Browse files Browse the repository at this point in the history
  • Loading branch information
fallwith committed Oct 20, 2023
2 parents 914302d + aab41ab commit 40b5fd2
Show file tree
Hide file tree
Showing 20 changed files with 394 additions and 45 deletions.
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

## dev

Version <dev> adds instrumentation for Ethon, gleans Docker container IDs from cgroups v2-based containers, records additional synthetics attributes, fixes an issue with Rails 7.1 that could cause duplicate log records to be sent to New Relic, and fixes a deprecation warning for the Sidekiq error handler.
Version <dev> adds instrumentation for Async::HTTP and Ethon, gleans Docker container IDs from cgroups v2-based containers, records additional synthetics attributes, fixes an issue with Rails 7.1 that could cause duplicate log records to be sent to New Relic, and fixes a deprecation warning for the Sidekiq error handler.

- **Feature: Add instrumentation for Async::HTTP**

The agent will now record spans for Async::HTTP requests. Versions 0.59.0 and above of the async-http gem are supported. [PR#2272](https://github.com/newrelic/newrelic-ruby-agent/pull/2272)

- **Feature: Add instrumentation for Ethon**

Instrumentation has been added for the [Ethon](https://github.com/typhoeus/ethon) HTTP client gem. The agent will now record external request segments for invocations of `Ethon::Easy#perform` and `Ethon::Multi#perform`. NOTE: The [Typhoeus](https://github.com/typhoeus/typhoeus) gem is maintained by the same team that maintains Ethon and depends on Ethon for its functionality. To prevent duplicate reporting for each HTTP request, the Ethon instrumentation will be disabled when Typhoeus is detected. [PR#2260](https://github.com/newrelic/newrelic-ruby-agent/pull/2260)
Instrumentation has been added for the [Ethon](https://github.com/typhoeus/ethon) HTTP client gem. Versions 0.12.0 and above are supported. The agent will now record external request segments for invocations of `Ethon::Easy#perform` and `Ethon::Multi#perform`. NOTE: The [Typhoeus](https://github.com/typhoeus/typhoeus) gem is maintained by the same team that maintains Ethon and depends on Ethon for its functionality. To prevent duplicate reporting for each HTTP request, the Ethon instrumentation will be disabled when Typhoeus is detected. [PR#2260](https://github.com/newrelic/newrelic-ruby-agent/pull/2260)

- **Feature: Prevent the agent from starting in rails commands in Rails 7**

Expand Down
4 changes: 3 additions & 1 deletion lib/new_relic/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,9 @@ def add_custom_attributes(params) # THREAD_LOCAL_ACCESS
def add_new_segment_attributes(params, segment)
# Make sure not to override existing segment-level custom attributes
segment_custom_keys = segment.attributes.custom_attributes.keys.map(&:to_sym)
segment.add_custom_attributes(params.reject { |k, _v| segment_custom_keys.include?(k.to_sym) })
segment.add_custom_attributes(params.reject do |k, _v|
segment_custom_keys.include?(k.to_sym) if k.respond_to?(:to_sym) # param keys can be integers
end)
end

# Add custom attributes to the span event for the current span. Attributes will be visible on spans in the
Expand Down
8 changes: 8 additions & 0 deletions lib/new_relic/agent/configuration/default_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1379,6 +1379,14 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil)
:allowed_from_server => false,
:description => 'Controls auto-instrumentation of `ActiveSupport::Logger` at start up. May be one of: `auto`, `prepend`, `chain`, `disabled`. Used in Rails versions below 7.1.'
},
:'instrumentation.async_http' => {
:default => 'auto',
:public => true,
:type => String,
:dynamic_name => true,
:allowed_from_server => false,
:description => 'Controls auto-instrumentation of Async::HTTP at start up. May be one of: `auto`, `prepend`, `chain`, `disabled`.'
},
:'instrumentation.bunny' => {
:default => 'auto',
:public => true,
Expand Down
83 changes: 83 additions & 0 deletions lib/new_relic/agent/http_clients/async_http_wrappers.rb
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
26 changes: 26 additions & 0 deletions lib/new_relic/agent/instrumentation/async_http.rb
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
23 changes: 23 additions & 0 deletions lib/new_relic/agent/instrumentation/async_http/chain.rb
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 lib/new_relic/agent/instrumentation/async_http/instrumentation.rb
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
15 changes: 15 additions & 0 deletions lib/new_relic/agent/instrumentation/async_http/prepend.rb
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
Expand Up @@ -34,6 +34,7 @@

subs = %w[send_file
send_data
send_stream
redirect_to
halted_callback
unpermitted_parameters]
Expand Down
2 changes: 1 addition & 1 deletion lib/new_relic/agent/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ def needs_middleware_summary_metrics?(name)
end

def finish
return unless state.is_execution_traced?
return unless state.is_execution_traced? && initial_segment

@end_time = Process.clock_gettime(Process::CLOCK_REALTIME)
@duration = @end_time - @start_time
Expand Down
6 changes: 5 additions & 1 deletion newrelic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,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
# one of: auto, prepend, chain, disabled.
# instrumentation.concurrent_ruby: auto

Expand Down
2 changes: 1 addition & 1 deletion test/multiverse/lib/multiverse/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def execute_suites(filter, opts)
'database' => %w[elasticsearch mongo redis sequel],
'rails' => %w[active_record active_record_pg active_support_broadcast_logger active_support_logger rails rails_prepend activemerchant],
'frameworks' => %w[grape padrino roda sinatra],
'httpclients' => %w[curb excon httpclient],
'httpclients' => %w[async_http curb excon httpclient],
'httpclients_2' => %w[typhoeus net_http httprb ethon],
'infinite_tracing' => ['infinite_tracing'],

Expand Down
19 changes: 19 additions & 0 deletions test/multiverse/suites/async_http/Envfile
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)
Loading

0 comments on commit 40b5fd2

Please sign in to comment.