http_instrumentation 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2df5ce2bdf71e28c9a3c95afa298ef3d1d035d17dbc5b8afb8deced97b1d336c
4
- data.tar.gz: debfebac778a078bb34ccaa213389e1785bae0f02f116ba8d4aa2e1584dc26cb
3
+ metadata.gz: 2deee15a4609f8d50a706613af0805d53bc54beaf3e17f65b68f17c17453fed9
4
+ data.tar.gz: a3a76bc88fe4a5b799b59a807ac665dcd4b6f52f280236f7b8b804425a01846f
5
5
  SHA512:
6
- metadata.gz: b268d25d6e6a7e948b68e47d0e08762e3c4708a78feaa17821ebe65ae06d7f675c1ae26a45afb27c983fe5d625f47c4f4cdfa4023ef33b7d71deda6e163d5385
7
- data.tar.gz: 4fc09ff4a26388982f0f2b877c7827cd629bfd0f4905f71e6f31a1ab3c5dbd565b39da5cd96b185319daea8fc445b3b4cc58ef0a3744fe08c0a5eebe2d8e5fe3
6
+ metadata.gz: 932361a2de24b64de68929b61379e1d8f01acce64f3f03070070ccfb9da8a369329c7703f84f774e236c69a2343dde4c5a6add5fd21add2db9025cdde3515a7b
7
+ data.tar.gz: fa77a297e489a26894c6f49961a5f057bfb239385a93d96b0252892db411922e9e2c75d0abb9de663fe059d883d9e07f06a043ae27b7704a4e868c97a49d4eb8
data/CHANGELOG.md CHANGED
@@ -4,7 +4,34 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## 1.0.0 (unreleased)
7
+ ## 1.0.2
8
+
9
+ ### Fixed
10
+ - Requiring the gem now loads the ActiveSupport framework itself instead of just `active_support/notifications`, which is not self-sufficient on ActiveSupport 7+ and raised `NameError: uninitialized constant ActiveSupport::IsolatedExecutionState` on the first request if the host application had not already loaded ActiveSupport.
11
+ - The httpclient and excon instrumentation now forward blocks passed to the instrumented methods, so streaming response blocks are no longer silently ignored when the aliasing strategy is used.
12
+ - The excon instrumentation now encodes query parameters passed as a hash when building the URL for the event payload, so `access_token` parameters are properly stripped from the `:uri` payload value instead of leaking through in unparseable form.
13
+ - The aliased flag on instrumentation modules is now set before the instrumented method is swapped in, so a request on another thread can no longer hit a window during installation where it would raise `NoMethodError`.
14
+ - Removed the nonexistent `:net_http2` entry from `HTTPInstrumentation::IMPLEMENTATIONS`; no hook for it was ever implemented.
15
+ - `HTTPInstrumentation.initialize!` now accepts a single symbol for the `only` option instead of raising `NoMethodError`.
16
+ - The `except` option to `HTTPInstrumentation.initialize!` now excludes the listed libraries instead of instrumenting only those libraries.
17
+ - Requests made with relative URLs (e.g. a patron session with a `base_url`) no longer raise `URI::InvalidURIError` from the instrumentation after the request completes.
18
+ - Installing the instrumentation is now protected by a mutex so concurrent calls to `initialize!` cannot install the aliased methods twice, which would have caused infinite recursion on subsequent requests.
19
+ - The excon instrumentation now reports per-request values (path, query, etc.) instead of letting the connection's defaults override them in the event payload.
20
+ - The event payload now always includes the `:count` key even when the request raises an error.
21
+ - The net/http instrumentation no longer lets an error raised while building the event payload escape to the caller, matching the behavior of the other hooks.
22
+ - The ethon instrumentation clears the request info captured from `http_request` after each `perform`, so reusing an easy handle without going through `http_request` no longer reports the previous request's method and URL. The URL set directly on the handle is now also reported as a fallback.
23
+ - The `:url` payload value no longer includes an explicit port for URLs using the default port for their scheme.
24
+
25
+ ### Changed
26
+ - The minimum required Ruby version has been updated to 2.6.
27
+
28
+ ## 1.0.1
29
+
30
+ ### Fixed
31
+ - Added detection for other gems that may be instrumenting these same libraries and ensuring that the method of injecting the instrumentation call is compatible. This fixes issues caused by the fundamental incompatibility in Ruby between `alias_method` and `prepend` on the same method. Aliasing is now the default method for injection since this is the most compatible. However if another library has already prepended behavior on a method, then `prepend` will be used instead.
32
+ - `HTTPInstrumentation.client` properly returns the block return value if it was called with a block.
33
+
34
+ ## 1.0.0
8
35
 
9
36
  ### Added
10
37
  - Initial release.
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Continuous Integration](https://github.com/bdurand/http_instrumentation/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/http_instrumentation/actions/workflows/continuous_integration.yml)
4
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+ [![Gem Version](https://badge.fury.io/rb/http_instrumentation.svg)](https://badge.fury.io/rb/http_instrumentation)
5
6
 
6
7
  This gem adds instrumentation to a variety of the most commonly used Ruby HTTP client libraries via ActiveSupport notifications. The goal is to add a common instrumentation interface across all the HTTP client libraries used by an application (including ones installed as dependencies of other gems).
7
8
 
@@ -19,6 +20,8 @@ This gem adds instrumentation to a variety of the most commonly used Ruby HTTP c
19
20
 
20
21
  Note that several other popular HTTP client libraries like [Faraday](https://github.com/lostisland/faraday), [HTTParty](https://github.com/jnunemaker/httparty), and [RestClient](https://github.com/rest-client/rest-client) are built on top of these low level libraries.
21
22
 
23
+ A few code paths are not instrumented: curb requests that don't go through `Curl::Easy#http` (e.g. the native `http_get` and `http_post` helpers) and httpclient streaming responses.
24
+
22
25
  ## Usage
23
26
 
24
27
  To capture information about HTTP requests, simply subscribe to the `request.http` events with [ActiveSupport notifications](https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) (note that you should really use `monotonic_subscribe` instead of `subscribe` to avoid issues with clock adjustments).
@@ -63,6 +66,17 @@ HTTPX.get("https://example.com/r1", "https://example.com/r2")
63
66
  # => HTTP request: client: httpx, count: 2, duration: 150ms
64
67
  ```
65
68
 
69
+ ### Instrumenting Libraries Loaded Later
70
+
71
+ All supported libraries that have already been loaded are automatically instrumented when this gem is required. Instrumentation cannot be removed once it has been installed.
72
+
73
+ If an HTTP client library is loaded after this gem, you can call `HTTPInstrumentation.initialize!` again to instrument it. The `only` and `except` options can be used to limit which libraries are instrumented.
74
+
75
+ ```ruby
76
+ require "httpx"
77
+ HTTPInstrumentation.initialize!(only: [:httpx])
78
+ ```
79
+
66
80
  ### Security
67
81
 
68
82
  The `:uri` element in the event payload will be sanitized to remove any user/password elements encoded in the URL as well as any `access_token` query parameters.
@@ -115,7 +129,7 @@ You can also take advantage of the existing instrumentation and just override th
115
129
  ```ruby
116
130
  class MyHttpClient
117
131
  def get(url)
118
- HTTPInstrumentation.client("my_client")
132
+ HTTPInstrumentation.client("my_client") do
119
133
  Net::HTTP.get(URI(url))
120
134
  end
121
135
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.0.2
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
29
29
 
30
30
  spec.require_paths = ["lib"]
31
31
 
32
- spec.required_ruby_version = ">= 2.5"
32
+ spec.required_ruby_version = ">= 2.6"
33
33
 
34
34
  spec.add_dependency "activesupport", ">= 4.2"
35
35
 
@@ -11,17 +11,23 @@ module HTTPInstrumentation
11
11
  module CurbHook
12
12
  class << self
13
13
  def instrument!
14
- Instrumentation.instrument!(::Curl::Easy, self) if defined?(::Curl::Easy)
14
+ Instrumentation.instrument!(::Curl::Easy, self, :http) if defined?(::Curl::Easy)
15
15
  end
16
16
 
17
17
  def installed?
18
18
  !!(defined?(::Curl::Easy) && ::Curl::Easy.include?(self))
19
19
  end
20
+
21
+ attr_accessor :aliased
20
22
  end
21
23
 
22
- def http(method, *)
24
+ def http(method, *args)
23
25
  HTTPInstrumentation.instrument("curb") do |payload|
24
- retval = super
26
+ retval = if HTTPInstrumentation::Instrumentation::CurbHook.aliased
27
+ http_without_http_instrumentation(method, *args)
28
+ else
29
+ super
30
+ end
25
31
 
26
32
  payload[:http_method] = method
27
33
  begin
@@ -11,8 +11,8 @@ module HTTPInstrumentation
11
11
  module EthonHook
12
12
  class << self
13
13
  def instrument!
14
- Instrumentation.instrument!(::Ethon::Easy, Easy) if defined?(::Ethon::Easy)
15
- Instrumentation.instrument!(::Ethon::Multi, Multi) if defined?(::Ethon::Multi)
14
+ Instrumentation.instrument!(::Ethon::Easy, Easy, [:http_request, :perform]) if defined?(::Ethon::Easy)
15
+ Instrumentation.instrument!(::Ethon::Multi, Multi, :perform) if defined?(::Ethon::Multi)
16
16
  end
17
17
 
18
18
  def installed?
@@ -24,31 +24,60 @@ module HTTPInstrumentation
24
24
  end
25
25
 
26
26
  module Multi
27
- def perform(*)
27
+ class << self
28
+ attr_accessor :aliased
29
+ end
30
+
31
+ def perform(*args)
28
32
  HTTPInstrumentation.instrument("ethon") do |payload|
29
33
  begin
30
34
  payload[:count] = easy_handles.size
31
35
  rescue
32
36
  end
33
37
 
34
- super
38
+ if HTTPInstrumentation::Instrumentation::EthonHook::Multi.aliased
39
+ perform_without_http_instrumentation(*args)
40
+ else
41
+ super
42
+ end
35
43
  end
36
44
  end
37
45
  end
38
46
 
39
47
  module Easy
40
- def http_request(url, action_name, *)
41
- @http_method = action_name
42
- @http_url = url
43
- super
48
+ class << self
49
+ attr_accessor :aliased
44
50
  end
45
51
 
46
- def perform(*)
52
+ def http_request(url, action_name, *args)
53
+ @http_instrumentation_method = action_name
54
+ @http_instrumentation_url = url
55
+ if HTTPInstrumentation::Instrumentation::EthonHook::Easy.aliased
56
+ http_request_without_http_instrumentation(url, action_name, *args)
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def perform(*args)
47
63
  HTTPInstrumentation.instrument("ethon") do |payload|
48
- retval = super
64
+ payload[:http_method] = @http_instrumentation_method
65
+ begin
66
+ payload[:url] = (@http_instrumentation_url || url)
67
+ rescue
68
+ payload[:url] = @http_instrumentation_url
69
+ end
70
+ # Clear the request info so it isn't reported again if the handle
71
+ # is reused without going through http_request.
72
+ @http_instrumentation_method = nil
73
+ @http_instrumentation_url = nil
74
+
75
+ retval = if HTTPInstrumentation::Instrumentation::EthonHook::Easy.aliased
76
+ perform_without_http_instrumentation(*args)
77
+ else
78
+ super
79
+ end
49
80
 
50
- payload[:http_method] = @http_method
51
- payload[:url] = @http_url
52
81
  begin
53
82
  payload[:status_code] = response_code
54
83
  rescue
@@ -11,31 +11,41 @@ module HTTPInstrumentation
11
11
  module ExconHook
12
12
  class << self
13
13
  def instrument!
14
- Instrumentation.instrument!(::Excon::Connection, self) if defined?(::Excon::Connection)
14
+ Instrumentation.instrument!(::Excon::Connection, self, :request) if defined?(::Excon::Connection)
15
15
  end
16
16
 
17
17
  def installed?
18
18
  !!(defined?(::Excon::Connection) && ::Excon::Connection.include?(self))
19
19
  end
20
+
21
+ attr_accessor :aliased
20
22
  end
21
23
 
22
- def request(params = {}, *)
24
+ def request(params = {}, &block)
23
25
  HTTPInstrumentation.instrument("excon") do |payload|
24
- response = super
26
+ response = if HTTPInstrumentation::Instrumentation::ExconHook.aliased
27
+ request_without_http_instrumentation(params, &block)
28
+ else
29
+ super
30
+ end
25
31
 
26
32
  begin
27
33
  info = params
34
+ # Merge connection defaults under the per-request params so the
35
+ # request values win.
28
36
  if respond_to?(:connection)
29
- info = info.merge(connection)
37
+ info = connection.merge(params)
30
38
  elsif respond_to?(:data)
31
- info = info.merge(data)
39
+ info = data.merge(params)
32
40
  end
33
41
 
34
42
  scheme = info[:scheme]&.downcase
35
43
  default_port = ((scheme == "https") ? 443 : 80)
36
44
  port = info[:port]
45
+ query = info[:query]
46
+ query = URI.encode_www_form(query) if query.is_a?(Hash)
37
47
  payload[:http_method] = (info[:http_method] || info[:method])
38
- payload[:url] = "#{scheme}://#{info[:host]}#{":#{port}" unless port == default_port}#{info[:path]}#{"?#{info[:query]}" unless info[:query].to_s.empty?}"
48
+ payload[:url] = "#{scheme}://#{info[:host]}#{":#{port}" unless port == default_port}#{info[:path]}#{"?#{query}" unless query.to_s.empty?}"
39
49
  payload[:status_code] = response.status
40
50
  rescue
41
51
  end
@@ -11,17 +11,23 @@ module HTTPInstrumentation
11
11
  module HTTPHook
12
12
  class << self
13
13
  def instrument!
14
- Instrumentation.instrument!(::HTTP::Client, self) if defined?(::HTTP::Client)
14
+ Instrumentation.instrument!(::HTTP::Client, self, :perform) if defined?(::HTTP::Client)
15
15
  end
16
16
 
17
17
  def installed?
18
18
  !!(defined?(::HTTP::Client) && ::HTTP::Client.include?(self))
19
19
  end
20
+
21
+ attr_accessor :aliased
20
22
  end
21
23
 
22
- def perform(request, *)
24
+ def perform(request, *args)
23
25
  HTTPInstrumentation.instrument("http") do |payload|
24
- response = super
26
+ response = if HTTPInstrumentation::Instrumentation::HTTPHook.aliased
27
+ perform_without_http_instrumentation(request, *args)
28
+ else
29
+ super
30
+ end
25
31
 
26
32
  begin
27
33
  payload[:http_method] = request.verb
@@ -11,17 +11,23 @@ module HTTPInstrumentation
11
11
  # This module is responsible for instrumenting the httpclient gem.
12
12
  class << self
13
13
  def instrument!
14
- Instrumentation.instrument!(::HTTPClient, self) if defined?(::HTTPClient)
14
+ Instrumentation.instrument!(::HTTPClient, self, :do_get_block) if defined?(::HTTPClient)
15
15
  end
16
16
 
17
17
  def installed?
18
18
  !!(defined?(::HTTPClient) && ::HTTPClient.include?(self))
19
19
  end
20
+
21
+ attr_accessor :aliased
20
22
  end
21
23
 
22
- def do_get_block(request, *)
24
+ def do_get_block(request, *args, &block)
23
25
  HTTPInstrumentation.instrument("httpclient") do |payload|
24
- response = super
26
+ response = if HTTPInstrumentation::Instrumentation::HTTPClientHook.aliased
27
+ do_get_block_without_http_instrumentation(request, *args, &block)
28
+ else
29
+ super
30
+ end
25
31
 
26
32
  begin
27
33
  payload[:http_method] = request.header.request_method
@@ -11,19 +11,25 @@ module HTTPInstrumentation
11
11
  module HTTPXHook
12
12
  class << self
13
13
  def instrument!
14
- Instrumentation.instrument!(::HTTPX::Session, self) if defined?(::HTTPX::Session)
14
+ Instrumentation.instrument!(::HTTPX::Session, self, :send_requests) if defined?(::HTTPX::Session)
15
15
  end
16
16
 
17
17
  def installed?
18
18
  !!(defined?(::HTTPX::Session) && ::HTTPX::Session.include?(self))
19
19
  end
20
+
21
+ attr_accessor :aliased
20
22
  end
21
23
 
22
24
  private
23
25
 
24
26
  def send_requests(*requests)
25
27
  HTTPInstrumentation.instrument("httpx") do |payload|
26
- responses = super
28
+ responses = if HTTPInstrumentation::Instrumentation::HTTPXHook.aliased
29
+ send_requests_without_http_instrumentation(*requests)
30
+ else
31
+ super
32
+ end
27
33
 
28
34
  if requests.size == 1
29
35
  begin
@@ -11,26 +11,43 @@ module HTTPInstrumentation
11
11
  module NetHTTPHook
12
12
  class << self
13
13
  def instrument!
14
- Instrumentation.instrument!(::Net::HTTP, self) if defined?(::Net::HTTP)
14
+ Instrumentation.instrument!(::Net::HTTP, self, :request) if defined?(::Net::HTTP)
15
15
  end
16
16
 
17
17
  def installed?
18
18
  !!(defined?(::Net::HTTP) && ::Net::HTTP.include?(self))
19
19
  end
20
+
21
+ attr_accessor :aliased
20
22
  end
21
23
 
22
- def request(req, *)
23
- return super unless started?
24
+ def request(req, *args, &block)
25
+ unless started?
26
+ if HTTPInstrumentation::Instrumentation::NetHTTPHook.aliased
27
+ return request_without_http_instrumentation(req, *args, &block)
28
+ else
29
+ return super
30
+ end
31
+ end
24
32
 
25
33
  HTTPInstrumentation.instrument("net/http") do |payload|
26
- response = super
27
-
28
- default_port = (use_ssl? ? 443 : 80)
29
- scheme = (use_ssl? ? "https" : "http")
30
- url = "#{scheme}://#{address}#{":#{port}" unless port == default_port}#{req.path}"
31
- payload[:http_method] = req.method
32
- payload[:url] = url
33
- payload[:status_code] = response.code
34
+ response = if HTTPInstrumentation::Instrumentation::NetHTTPHook.aliased
35
+ request_without_http_instrumentation(req, *args, &block)
36
+ else
37
+ super
38
+ end
39
+
40
+ begin
41
+ default_port = (use_ssl? ? 443 : 80)
42
+ scheme = (use_ssl? ? "https" : "http")
43
+ url = "#{scheme}://#{address}#{":#{port}" unless port == default_port}#{req.path}"
44
+ payload[:http_method] = req.method
45
+ payload[:url] = url
46
+ payload[:status_code] = response.code
47
+ rescue
48
+ # Instrumentation must never break the request; if the payload can't
49
+ # be built, return the response without the extra fields.
50
+ end
34
51
 
35
52
  response
36
53
  end
@@ -11,19 +11,25 @@ module HTTPInstrumentation
11
11
  module PatronHook
12
12
  class << self
13
13
  def instrument!
14
- Instrumentation.instrument!(::Patron::Session, self) if defined?(::Patron::Session)
14
+ Instrumentation.instrument!(::Patron::Session, self, :request) if defined?(::Patron::Session)
15
15
  end
16
16
 
17
17
  def installed?
18
18
  !!(defined?(::Patron::Session) && ::Patron::Session.include?(self))
19
19
  end
20
+
21
+ attr_accessor :aliased
20
22
  end
21
23
 
22
24
  private
23
25
 
24
- def request(action, url, *)
26
+ def request(action, url, *args)
25
27
  HTTPInstrumentation.instrument("patron") do |payload|
26
- response = super
28
+ response = if HTTPInstrumentation::Instrumentation::PatronHook.aliased
29
+ request_without_http_instrumentation(action, url, *args)
30
+ else
31
+ super
32
+ end
27
33
 
28
34
  payload[:http_method] = action
29
35
  payload[:url] = url
@@ -11,8 +11,8 @@ module HTTPInstrumentation
11
11
  module TyphoeusHook
12
12
  class << self
13
13
  def instrument!
14
- Instrumentation.instrument!(::Typhoeus::Request, Easy) if defined?(::Typhoeus::Request)
15
- Instrumentation.instrument!(::Typhoeus::Hydra, Multi) if defined?(::Typhoeus::Hydra)
14
+ Instrumentation.instrument!(::Typhoeus::Request, Easy, :run) if defined?(::Typhoeus::Request)
15
+ Instrumentation.instrument!(::Typhoeus::Hydra, Multi, :run) if defined?(::Typhoeus::Hydra)
16
16
  end
17
17
 
18
18
  def installed?
@@ -24,22 +24,38 @@ module HTTPInstrumentation
24
24
  end
25
25
 
26
26
  module Multi
27
- def run(*)
27
+ class << self
28
+ attr_accessor :aliased
29
+ end
30
+
31
+ def run(*args)
28
32
  HTTPInstrumentation.instrument("typhoeus") do |payload|
29
33
  begin
30
34
  payload[:count] = queued_requests.size
31
35
  rescue
32
36
  end
33
37
 
34
- super
38
+ if HTTPInstrumentation::Instrumentation::TyphoeusHook::Multi.aliased
39
+ run_without_http_instrumentation(*args)
40
+ else
41
+ super
42
+ end
35
43
  end
36
44
  end
37
45
  end
38
46
 
39
47
  module Easy
40
- def run(*)
48
+ class << self
49
+ attr_accessor :aliased
50
+ end
51
+
52
+ def run(*args)
41
53
  HTTPInstrumentation.instrument("typhoeus") do |payload|
42
- retval = super
54
+ retval = if HTTPInstrumentation::Instrumentation::TyphoeusHook::Easy.aliased
55
+ run_without_http_instrumentation(*args)
56
+ else
57
+ super
58
+ end
43
59
 
44
60
  begin
45
61
  payload[:http_method] = options[:method]
@@ -12,10 +12,81 @@ require_relative "instrumentation/typhoeus_hook"
12
12
 
13
13
  module HTTPInstrumentation
14
14
  module Instrumentation
15
+ # Lock to prevent concurrent calls from installing the same hooks twice;
16
+ # double aliasing would point the _without_http_instrumentation method at
17
+ # the instrumented method itself, causing infinite recursion.
18
+ INSTRUMENT_LOCK = Mutex.new
19
+ private_constant :INSTRUMENT_LOCK
20
+
15
21
  class << self
16
- # Helper method to prepend an instrumentation module to a class.
17
- def instrument!(klass, instrumentation_module)
18
- klass.prepend(instrumentation_module) unless klass.include?(instrumentation_module)
22
+ # Helper method to add an instrumentation module to methods on a class. The
23
+ # methods must be defined in the instrumentation module.
24
+ #
25
+ # If the methods have already been prepended on the class, then module will
26
+ # be prepended to the class. Otherwise, the methods will be aliased and the
27
+ # module will be included in the class. This is because prepending and aliasing
28
+ # methods are not compatible with each other and other instrumentation libraries
29
+ # may have already prepended the methods. Aliasing is the default strategy because
30
+ # prepending after aliasing will work, but aliasing after prepending will not.
31
+ def instrument!(klass, instrumentation_module, methods)
32
+ INSTRUMENT_LOCK.synchronize do
33
+ return if klass.include?(instrumentation_module)
34
+
35
+ methods = Array(methods).collect(&:to_sym)
36
+
37
+ if HTTPInstrumentation.force_prepend? || methods_defined_in_module?(klass, methods)
38
+ klass.prepend(instrumentation_module)
39
+ instrumentation_module.aliased = false
40
+ else
41
+ methods.each do |method|
42
+ instrumentation_module.alias_method("#{method}_with_http_instrumentation", method)
43
+ end
44
+
45
+ klass.include(instrumentation_module)
46
+
47
+ swap_methods = methods.reject { |method| defines_method?(klass, "#{method}_without_http_instrumentation") }
48
+
49
+ swap_methods.each do |method|
50
+ klass.alias_method("#{method}_without_http_instrumentation", method)
51
+ end
52
+
53
+ # The aliased flag must be set before the instrumented methods are
54
+ # swapped in; a call landing in between would take the super branch
55
+ # and raise NoMethodError since the module was included, not prepended.
56
+ instrumentation_module.aliased = true
57
+
58
+ swap_methods.each do |method|
59
+ klass.alias_method(method, "#{method}_with_http_instrumentation")
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ # Returns true if any of the methods are defined in a module in the class's
68
+ # ancestry rather than directly on the class itself. This covers both modules
69
+ # prepended in front of the class and modules included behind it. In either
70
+ # case the aliasing strategy cannot be used: including the instrumentation
71
+ # module would insert it ahead of the module that defines the method, so
72
+ # aliasing the method afterward would capture the instrumented method itself
73
+ # and cause infinite recursion.
74
+ def methods_defined_in_module?(klass, methods)
75
+ defined_in_module = false
76
+
77
+ klass.ancestors.each do |mod|
78
+ next unless mod.is_a?(Module) && !mod.is_a?(Class)
79
+
80
+ module_methods = mod.instance_methods(false) + mod.private_instance_methods(false)
81
+ defined_in_module = (module_methods & methods).any?
82
+ break if defined_in_module
83
+ end
84
+
85
+ defined_in_module
86
+ end
87
+
88
+ def defines_method?(klass, method)
89
+ klass.method_defined?(method) || klass.private_method_defined?(method)
19
90
  end
20
91
  end
21
92
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support"
3
4
  require "active_support/notifications"
4
5
 
5
6
  require_relative "http_instrumentation/instrumentation"
@@ -13,7 +14,6 @@ module HTTPInstrumentation
13
14
  :httpclient,
14
15
  :httpx,
15
16
  :net_http,
16
- :net_http2,
17
17
  :patron,
18
18
  :typhoeus
19
19
  ].freeze
@@ -29,8 +29,8 @@ module HTTPInstrumentation
29
29
  # @param except [Array<Symbol>] List of libraries to not instrument.
30
30
  # @return [void]
31
31
  def initialize!(only: nil, except: nil)
32
- list = (only || IMPLEMENTATIONS)
33
- list &= Array(except) if except
32
+ list = Array(only || IMPLEMENTATIONS)
33
+ list -= Array(except) if except
34
34
 
35
35
  Instrumentation::CurbHook.instrument! if list.include?(:curb)
36
36
  Instrumentation::EthonHook.instrument! if list.include?(:ethon)
@@ -45,6 +45,9 @@ module HTTPInstrumentation
45
45
 
46
46
  # Silence instrumentation for the duration of the block.
47
47
  #
48
+ # Note that silencing is stored in fiber-local storage, so it does not
49
+ # propagate into other threads or fibers spawned within the block.
50
+ #
48
51
  # @return [Object] the return value of the block
49
52
  def silence(&block)
50
53
  save_val = Thread.current[:http_instrumentation_silence]
@@ -60,18 +63,20 @@ module HTTPInstrumentation
60
63
  # block. If no block is given, then return the current HTTP client name.
61
64
  #
62
65
  # @param name [String, Symbol, nil] The name of the client to set
63
- # @return [String, Symbol, nil] The current name of the client
66
+ # @return [String, Symbol, nil] If a block is given, then the return value of the block. Otherwise
67
+ # the current client name.
64
68
  def client(name = nil)
65
69
  save_val = Thread.current[:http_instrumentation_client]
66
70
  if block_given?
67
71
  begin
68
- Thread.current[:http_instrumentation_client] = name
72
+ Thread.current[:http_instrumentation_client] = name&.to_s
69
73
  yield
70
74
  ensure
71
75
  Thread.current[:http_instrumentation_client] = save_val
72
76
  end
77
+ else
78
+ save_val
73
79
  end
74
- save_val
75
80
  end
76
81
 
77
82
  # Returns true if instrumentation is currently silenced.
@@ -99,29 +104,42 @@ module HTTPInstrumentation
99
104
  return yield(payload) if silenced?
100
105
 
101
106
  ActiveSupport::Notifications.instrument(EVENT, payload) do
102
- retval = silence { yield(payload) }
103
-
104
- payload[:http_method] = normalize_http_method(payload[:http_method]) if payload.include?(:http_method)
105
-
106
- if payload.include?(:url)
107
- uri = sanitized_uri(payload[:url])
108
- if uri
109
- payload[:url] = uri_without_query_string(uri)
110
- payload[:uri] = uri
111
- else
112
- payload[:url] = payload[:url]&.to_s
113
- end
114
- end
107
+ instrument_request(payload, &block)
108
+ end
109
+ end
115
110
 
116
- payload[:status_code] = payload[:status_code]&.to_i if payload.include?(:status_code)
111
+ # Returns true if instrumentation should always use module prepend rather than method aliasing.
112
+ # The default is to use method aliasing since that is more compatible with other libraries.
113
+ # Prepending can be forced by setting the HTTP_INSTRUMENTATION_FORCE_PREPEND environment variable
114
+ # to "true".
115
+ def force_prepend?
116
+ ENV.fetch("HTTP_INSTRUMENTATION_FORCE_PREPEND", nil) == "true"
117
+ end
117
118
 
118
- payload[:count] = (payload.include?(:count) ? payload[:count].to_i : 1)
119
+ private
119
120
 
120
- retval
121
+ # Yield the block with instrumentation silenced and then normalize the
122
+ # payload. The payload is normalized even if the block raises an error so
123
+ # that events for failed requests still include the normalized keys.
124
+ def instrument_request(payload)
125
+ silence { yield(payload) }
126
+ ensure
127
+ payload[:http_method] = normalize_http_method(payload[:http_method]) if payload.include?(:http_method)
128
+
129
+ if payload.include?(:url)
130
+ uri = sanitized_uri(payload[:url])
131
+ if uri
132
+ payload[:url] = uri_without_query_string(uri)
133
+ payload[:uri] = uri
134
+ else
135
+ payload[:url] = payload[:url]&.to_s
136
+ end
121
137
  end
122
- end
123
138
 
124
- private
139
+ payload[:status_code] = payload[:status_code]&.to_i if payload.include?(:status_code)
140
+
141
+ payload[:count] = (payload.include?(:count) ? payload[:count].to_i : 1)
142
+ end
125
143
 
126
144
  # Turn the given value into a lowercase symbol.
127
145
  def normalize_http_method(method)
@@ -161,7 +179,10 @@ module HTTPInstrumentation
161
179
  end
162
180
 
163
181
  def uri_without_query_string(uri)
164
- URI("#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}").to_s
182
+ stripped = uri.dup
183
+ stripped.query = nil if stripped.respond_to?(:query=)
184
+ stripped.fragment = nil if stripped.respond_to?(:fragment=)
185
+ stripped.to_s
165
186
  end
166
187
  end
167
188
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http_instrumentation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-07-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -38,7 +37,6 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
41
- description:
42
40
  email:
43
41
  - bbdurand@gmail.com
44
42
  executables: []
@@ -65,7 +63,6 @@ homepage: https://github.com/bdurand/http_instrumentation
65
63
  licenses:
66
64
  - MIT
67
65
  metadata: {}
68
- post_install_message:
69
66
  rdoc_options: []
70
67
  require_paths:
71
68
  - lib
@@ -73,15 +70,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
73
70
  requirements:
74
71
  - - ">="
75
72
  - !ruby/object:Gem::Version
76
- version: '2.5'
73
+ version: '2.6'
77
74
  required_rubygems_version: !ruby/object:Gem::Requirement
78
75
  requirements:
79
76
  - - ">="
80
77
  - !ruby/object:Gem::Version
81
78
  version: '0'
82
79
  requirements: []
83
- rubygems_version: 3.4.12
84
- signing_key:
80
+ rubygems_version: 4.0.3
85
81
  specification_version: 4
86
82
  summary: ActiveSupoprt instrumentation for a variety of Ruby HTTP client libraries.
87
83
  test_files: []