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 +4 -4
- data/CHANGELOG.md +28 -1
- data/README.md +15 -1
- data/VERSION +1 -1
- data/http_instrumentation.gemspec +1 -1
- data/lib/http_instrumentation/instrumentation/curb_hook.rb +9 -3
- data/lib/http_instrumentation/instrumentation/ethon_hook.rb +41 -12
- data/lib/http_instrumentation/instrumentation/excon_hook.rb +16 -6
- data/lib/http_instrumentation/instrumentation/http_hook.rb +9 -3
- data/lib/http_instrumentation/instrumentation/httpclient_hook.rb +9 -3
- data/lib/http_instrumentation/instrumentation/httpx_hook.rb +8 -2
- data/lib/http_instrumentation/instrumentation/net_http_hook.rb +28 -11
- data/lib/http_instrumentation/instrumentation/patron_hook.rb +9 -3
- data/lib/http_instrumentation/instrumentation/typhoeus_hook.rb +22 -6
- data/lib/http_instrumentation/instrumentation.rb +74 -3
- data/lib/http_instrumentation.rb +46 -25
- metadata +4 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2deee15a4609f8d50a706613af0805d53bc54beaf3e17f65b68f17c17453fed9
|
|
4
|
+
data.tar.gz: a3a76bc88fe4a5b799b59a807ac665dcd4b6f52f280236f7b8b804425a01846f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
[](https://github.com/bdurand/http_instrumentation/actions/workflows/continuous_integration.yml)
|
|
4
4
|
[](https://github.com/testdouble/standard)
|
|
5
|
+
[](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.
|
|
1
|
+
1.0.2
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
@http_url = url
|
|
43
|
-
super
|
|
48
|
+
class << self
|
|
49
|
+
attr_accessor :aliased
|
|
44
50
|
end
|
|
45
51
|
|
|
46
|
-
def
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
37
|
+
info = connection.merge(params)
|
|
30
38
|
elsif respond_to?(:data)
|
|
31
|
-
info =
|
|
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]}#{"?#{
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
class << self
|
|
49
|
+
attr_accessor :aliased
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def run(*args)
|
|
41
53
|
HTTPInstrumentation.instrument("typhoeus") do |payload|
|
|
42
|
-
retval =
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
data/lib/http_instrumentation.rb
CHANGED
|
@@ -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
|
|
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]
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
+
private
|
|
119
120
|
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
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:
|
|
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: []
|