http_instrumentation 1.0.0
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 +7 -0
- data/CHANGELOG.md +10 -0
- data/MIT-LICENSE.txt +20 -0
- data/README.md +154 -0
- data/VERSION +1 -0
- data/http_instrumentation.gemspec +37 -0
- data/lib/http_instrumentation/instrumentation/curb_hook.rb +38 -0
- data/lib/http_instrumentation/instrumentation/ethon_hook.rb +63 -0
- data/lib/http_instrumentation/instrumentation/excon_hook.rb +48 -0
- data/lib/http_instrumentation/instrumentation/http_hook.rb +38 -0
- data/lib/http_instrumentation/instrumentation/httpclient_hook.rb +38 -0
- data/lib/http_instrumentation/instrumentation/httpx_hook.rb +44 -0
- data/lib/http_instrumentation/instrumentation/net_http_hook.rb +40 -0
- data/lib/http_instrumentation/instrumentation/patron_hook.rb +40 -0
- data/lib/http_instrumentation/instrumentation/typhoeus_hook.rb +57 -0
- data/lib/http_instrumentation/instrumentation.rb +22 -0
- data/lib/http_instrumentation.rb +169 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2df5ce2bdf71e28c9a3c95afa298ef3d1d035d17dbc5b8afb8deced97b1d336c
|
4
|
+
data.tar.gz: debfebac778a078bb34ccaa213389e1785bae0f02f116ba8d4aa2e1584dc26cb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b268d25d6e6a7e948b68e47d0e08762e3c4708a78feaa17821ebe65ae06d7f675c1ae26a45afb27c983fe5d625f47c4f4cdfa4023ef33b7d71deda6e163d5385
|
7
|
+
data.tar.gz: 4fc09ff4a26388982f0f2b877c7827cd629bfd0f4905f71e6f31a1ab3c5dbd565b39da5cd96b185319daea8fc445b3b4cc58ef0a3744fe08c0a5eebe2d8e5fe3
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## 1.0.0 (unreleased)
|
8
|
+
|
9
|
+
### Added
|
10
|
+
- Initial release.
|
data/MIT-LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2023 Brian Durand
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
# HTTP Instrumentation
|
2
|
+
|
3
|
+
[](https://github.com/bdurand/http_instrumentation/actions/workflows/continuous_integration.yml)
|
4
|
+
[](https://github.com/testdouble/standard)
|
5
|
+
|
6
|
+
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
|
+
### Supported Libraries
|
9
|
+
|
10
|
+
* [net/http](https://docs.ruby-lang.org/en/master/Net/HTTP.html) (from the Ruby standard library)
|
11
|
+
* [curb](https://github.com/taf2/curb)
|
12
|
+
* [ethon](https://github.com/typhoeus/ethon)
|
13
|
+
* [excon](https://github.com/excon/excon)
|
14
|
+
* [http](https://github.com/httprb/http) (a.k.a. http.rb)
|
15
|
+
* [httpclient](https://github.com/nahi/httpclient)
|
16
|
+
* [httpx](https://github.com/HoneyryderChuck/httpx)
|
17
|
+
* [patron](https://github.com/toland/patron)
|
18
|
+
* [typhoeus](https://github.com/typhoeus/typhoeus)
|
19
|
+
|
20
|
+
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
|
+
## Usage
|
23
|
+
|
24
|
+
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).
|
25
|
+
|
26
|
+
The payload on event notifications for all HTTP requests will include:
|
27
|
+
|
28
|
+
* `:client` - The client library used to make the request
|
29
|
+
* `:count` - The number of HTTP requests that were made
|
30
|
+
|
31
|
+
If a single HTTP request was made, then these keys will exist as well:
|
32
|
+
|
33
|
+
* `:uri` - The URI for the request
|
34
|
+
* `:url` - The URL for the request with any query string stripped off
|
35
|
+
* `:http_method` - The HTTP method for the request
|
36
|
+
* `:status_code` - The numeric HTTP status code for the response
|
37
|
+
|
38
|
+
These additional values will not be present if multiple, concurrent requests were made. Only the typhoeus, ethon, and httpx libraries support making concurrent requests.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
ActiveSupport::Notifications.monotonic_subscribe("request.http") do |*args|
|
42
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
43
|
+
client = event.payload[:client]
|
44
|
+
count = event.payload[:count]
|
45
|
+
url = event.payload[:url]
|
46
|
+
uri = event.payload[:uri]
|
47
|
+
http_method = event.payload[:http_method]
|
48
|
+
status_code = event.payload[:status_code]
|
49
|
+
|
50
|
+
puts "HTTP request: client: #{client}, count: #{count}, duration: #{event.duration}ms"
|
51
|
+
if count == 1
|
52
|
+
puts "#{http_method} #{url} - status: #{status_code}, host: #{uri&.host}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Single request
|
57
|
+
Net::HTTP.get(URI("https://example.com/info"))
|
58
|
+
# => HTTP request: client: net/http, count: 1, duration: 100ms
|
59
|
+
# => GET https://example.com/info - status 200, host: example.com
|
60
|
+
|
61
|
+
# Multiple, concurrent requests
|
62
|
+
HTTPX.get("https://example.com/r1", "https://example.com/r2")
|
63
|
+
# => HTTP request: client: httpx, count: 2, duration: 150ms
|
64
|
+
```
|
65
|
+
|
66
|
+
### Security
|
67
|
+
|
68
|
+
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.
|
69
|
+
|
70
|
+
The `:url` element will also have the query string stripped from it so it will just include the scheme, host, and path.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
HTTP.get("https://user@password123@example.com/path")
|
74
|
+
HTTP.get("https://example.com/path?access_token=secrettoken")
|
75
|
+
# event.payload[:url] will be https://example.com/path in both cases
|
76
|
+
```
|
77
|
+
|
78
|
+
The hostname will also be converted to lowercase in these attributes.
|
79
|
+
|
80
|
+
### Silencing Notifications
|
81
|
+
|
82
|
+
If you want to suppress notifications, you can do so by surrounding code with an `HTTPInstrumentation.silence` block.
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
HTTPInstrumentation.silence do
|
86
|
+
HTTP.get("https://example.com/info") # Notification will not be sent
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
### Custom HTTP Clients
|
91
|
+
|
92
|
+
You can instrument additional HTTP calls with the `HTTPInstrumentation.instrument` method. Adding instrumentation to higher level clients will suppress any instrumentation from lower level clients they may be using so you'll only get one event per request.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class MyHttpClient
|
96
|
+
def get(url)
|
97
|
+
HTTPInstrumentation.instrument("my_client") do |payload|
|
98
|
+
response = Net::HTTP.get(URI(url))
|
99
|
+
|
100
|
+
payload[:http_method] = :get
|
101
|
+
payload[:url] = url
|
102
|
+
payload[:status_code] = response.code
|
103
|
+
|
104
|
+
response
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
MyHttpClient.get("https://example.com/")
|
110
|
+
# Event => {client: "my_client", http_method => :get, url: "https://example.com/"}
|
111
|
+
```
|
112
|
+
|
113
|
+
You can also take advantage of the existing instrumentation and just override the client name in the notification event.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class MyHttpClient
|
117
|
+
def get(url)
|
118
|
+
HTTPInstrumentation.client("my_client")
|
119
|
+
Net::HTTP.get(URI(url))
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
MyHttpClient.get("https://example.com/")
|
125
|
+
# Event => {client: "my_client", http_method => :get, url: "https://example.com/"}
|
126
|
+
```
|
127
|
+
|
128
|
+
## Installation
|
129
|
+
|
130
|
+
Add this line to your application's Gemfile:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
gem "http_instrumentation"
|
134
|
+
```
|
135
|
+
|
136
|
+
Then execute:
|
137
|
+
```bash
|
138
|
+
$ bundle
|
139
|
+
```
|
140
|
+
|
141
|
+
Or install it yourself as:
|
142
|
+
```bash
|
143
|
+
$ gem install http_instrumentation
|
144
|
+
```
|
145
|
+
|
146
|
+
## Contributing
|
147
|
+
|
148
|
+
Open a pull request on [GitHub](https://github.com/bdurand/http_instrumentation).
|
149
|
+
|
150
|
+
Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
|
151
|
+
|
152
|
+
## License
|
153
|
+
|
154
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,37 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "http_instrumentation"
|
3
|
+
spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
|
4
|
+
spec.authors = ["Brian Durand"]
|
5
|
+
spec.email = ["bbdurand@gmail.com"]
|
6
|
+
|
7
|
+
spec.summary = "ActiveSupoprt instrumentation for a variety of Ruby HTTP client libraries."
|
8
|
+
|
9
|
+
spec.homepage = "https://github.com/bdurand/http_instrumentation"
|
10
|
+
spec.license = "MIT"
|
11
|
+
|
12
|
+
# Specify which files should be added to the gem when it is released.
|
13
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
14
|
+
ignore_files = %w[
|
15
|
+
.
|
16
|
+
Appraisals
|
17
|
+
Gemfile
|
18
|
+
Gemfile.lock
|
19
|
+
Rakefile
|
20
|
+
config.ru
|
21
|
+
assets/
|
22
|
+
bin/
|
23
|
+
gemfiles/
|
24
|
+
spec/
|
25
|
+
]
|
26
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
27
|
+
`git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
|
28
|
+
end
|
29
|
+
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.required_ruby_version = ">= 2.5"
|
33
|
+
|
34
|
+
spec.add_dependency "activesupport", ">= 4.2"
|
35
|
+
|
36
|
+
spec.add_development_dependency "bundler"
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "curb"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module HTTPInstrumentation
|
9
|
+
module Instrumentation
|
10
|
+
# This module is responsible for instrumenting the curb gem.
|
11
|
+
module CurbHook
|
12
|
+
class << self
|
13
|
+
def instrument!
|
14
|
+
Instrumentation.instrument!(::Curl::Easy, self) if defined?(::Curl::Easy)
|
15
|
+
end
|
16
|
+
|
17
|
+
def installed?
|
18
|
+
!!(defined?(::Curl::Easy) && ::Curl::Easy.include?(self))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def http(method, *)
|
23
|
+
HTTPInstrumentation.instrument("curb") do |payload|
|
24
|
+
retval = super
|
25
|
+
|
26
|
+
payload[:http_method] = method
|
27
|
+
begin
|
28
|
+
payload[:url] = url
|
29
|
+
payload[:status_code] = response_code
|
30
|
+
rescue
|
31
|
+
end
|
32
|
+
|
33
|
+
retval
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "ethon"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module HTTPInstrumentation
|
9
|
+
module Instrumentation
|
10
|
+
# This module is responsible for instrumenting the ethon gem.
|
11
|
+
module EthonHook
|
12
|
+
class << self
|
13
|
+
def instrument!
|
14
|
+
Instrumentation.instrument!(::Ethon::Easy, Easy) if defined?(::Ethon::Easy)
|
15
|
+
Instrumentation.instrument!(::Ethon::Multi, Multi) if defined?(::Ethon::Multi)
|
16
|
+
end
|
17
|
+
|
18
|
+
def installed?
|
19
|
+
!!(
|
20
|
+
defined?(::Ethon::Easy) && ::Ethon::Easy.include?(Easy) &&
|
21
|
+
defined?(::Ethon::Multi) && ::Ethon::Multi.include?(Multi)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Multi
|
27
|
+
def perform(*)
|
28
|
+
HTTPInstrumentation.instrument("ethon") do |payload|
|
29
|
+
begin
|
30
|
+
payload[:count] = easy_handles.size
|
31
|
+
rescue
|
32
|
+
end
|
33
|
+
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module Easy
|
40
|
+
def http_request(url, action_name, *)
|
41
|
+
@http_method = action_name
|
42
|
+
@http_url = url
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
def perform(*)
|
47
|
+
HTTPInstrumentation.instrument("ethon") do |payload|
|
48
|
+
retval = super
|
49
|
+
|
50
|
+
payload[:http_method] = @http_method
|
51
|
+
payload[:url] = @http_url
|
52
|
+
begin
|
53
|
+
payload[:status_code] = response_code
|
54
|
+
rescue
|
55
|
+
end
|
56
|
+
|
57
|
+
retval
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "excon"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module HTTPInstrumentation
|
9
|
+
module Instrumentation
|
10
|
+
# This module is used to instrument the excon gem.
|
11
|
+
module ExconHook
|
12
|
+
class << self
|
13
|
+
def instrument!
|
14
|
+
Instrumentation.instrument!(::Excon::Connection, self) if defined?(::Excon::Connection)
|
15
|
+
end
|
16
|
+
|
17
|
+
def installed?
|
18
|
+
!!(defined?(::Excon::Connection) && ::Excon::Connection.include?(self))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def request(params = {}, *)
|
23
|
+
HTTPInstrumentation.instrument("excon") do |payload|
|
24
|
+
response = super
|
25
|
+
|
26
|
+
begin
|
27
|
+
info = params
|
28
|
+
if respond_to?(:connection)
|
29
|
+
info = info.merge(connection)
|
30
|
+
elsif respond_to?(:data)
|
31
|
+
info = info.merge(data)
|
32
|
+
end
|
33
|
+
|
34
|
+
scheme = info[:scheme]&.downcase
|
35
|
+
default_port = ((scheme == "https") ? 443 : 80)
|
36
|
+
port = info[:port]
|
37
|
+
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?}"
|
39
|
+
payload[:status_code] = response.status
|
40
|
+
rescue
|
41
|
+
end
|
42
|
+
|
43
|
+
response
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "http"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module HTTPInstrumentation
|
9
|
+
module Instrumentation
|
10
|
+
# This module is responsible for instrumenting the http gem.
|
11
|
+
module HTTPHook
|
12
|
+
class << self
|
13
|
+
def instrument!
|
14
|
+
Instrumentation.instrument!(::HTTP::Client, self) if defined?(::HTTP::Client)
|
15
|
+
end
|
16
|
+
|
17
|
+
def installed?
|
18
|
+
!!(defined?(::HTTP::Client) && ::HTTP::Client.include?(self))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform(request, *)
|
23
|
+
HTTPInstrumentation.instrument("http") do |payload|
|
24
|
+
response = super
|
25
|
+
|
26
|
+
begin
|
27
|
+
payload[:http_method] = request.verb
|
28
|
+
payload[:url] = request.uri
|
29
|
+
payload[:status_code] = response.status
|
30
|
+
rescue
|
31
|
+
end
|
32
|
+
|
33
|
+
response
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "httpclient"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module HTTPInstrumentation
|
9
|
+
module Instrumentation
|
10
|
+
module HTTPClientHook
|
11
|
+
# This module is responsible for instrumenting the httpclient gem.
|
12
|
+
class << self
|
13
|
+
def instrument!
|
14
|
+
Instrumentation.instrument!(::HTTPClient, self) if defined?(::HTTPClient)
|
15
|
+
end
|
16
|
+
|
17
|
+
def installed?
|
18
|
+
!!(defined?(::HTTPClient) && ::HTTPClient.include?(self))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def do_get_block(request, *)
|
23
|
+
HTTPInstrumentation.instrument("httpclient") do |payload|
|
24
|
+
response = super
|
25
|
+
|
26
|
+
begin
|
27
|
+
payload[:http_method] = request.header.request_method
|
28
|
+
payload[:url] = request.header.request_uri
|
29
|
+
payload[:status_code] = response.header.status_code
|
30
|
+
rescue
|
31
|
+
end
|
32
|
+
|
33
|
+
response
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "httpx"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module HTTPInstrumentation
|
9
|
+
module Instrumentation
|
10
|
+
# This module is responsible for instrumenting the httpx gem.
|
11
|
+
module HTTPXHook
|
12
|
+
class << self
|
13
|
+
def instrument!
|
14
|
+
Instrumentation.instrument!(::HTTPX::Session, self) if defined?(::HTTPX::Session)
|
15
|
+
end
|
16
|
+
|
17
|
+
def installed?
|
18
|
+
!!(defined?(::HTTPX::Session) && ::HTTPX::Session.include?(self))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def send_requests(*requests)
|
25
|
+
HTTPInstrumentation.instrument("httpx") do |payload|
|
26
|
+
responses = super
|
27
|
+
|
28
|
+
if requests.size == 1
|
29
|
+
begin
|
30
|
+
payload[:http_method] = requests.first.verb
|
31
|
+
payload[:url] = requests.first.uri
|
32
|
+
payload[:status_code] = responses.first.status
|
33
|
+
rescue
|
34
|
+
end
|
35
|
+
else
|
36
|
+
payload[:count] = requests.size
|
37
|
+
end
|
38
|
+
|
39
|
+
responses
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "net/http"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module HTTPInstrumentation
|
9
|
+
module Instrumentation
|
10
|
+
# This module is responsible for instrumenting the net/http module in the standard library.
|
11
|
+
module NetHTTPHook
|
12
|
+
class << self
|
13
|
+
def instrument!
|
14
|
+
Instrumentation.instrument!(::Net::HTTP, self) if defined?(::Net::HTTP)
|
15
|
+
end
|
16
|
+
|
17
|
+
def installed?
|
18
|
+
!!(defined?(::Net::HTTP) && ::Net::HTTP.include?(self))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def request(req, *)
|
23
|
+
return super unless started?
|
24
|
+
|
25
|
+
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
|
+
|
35
|
+
response
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "patron"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module HTTPInstrumentation
|
9
|
+
module Instrumentation
|
10
|
+
# This module is responsible for instrumenting the patron gem.
|
11
|
+
module PatronHook
|
12
|
+
class << self
|
13
|
+
def instrument!
|
14
|
+
Instrumentation.instrument!(::Patron::Session, self) if defined?(::Patron::Session)
|
15
|
+
end
|
16
|
+
|
17
|
+
def installed?
|
18
|
+
!!(defined?(::Patron::Session) && ::Patron::Session.include?(self))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def request(action, url, *)
|
25
|
+
HTTPInstrumentation.instrument("patron") do |payload|
|
26
|
+
response = super
|
27
|
+
|
28
|
+
payload[:http_method] = action
|
29
|
+
payload[:url] = url
|
30
|
+
begin
|
31
|
+
payload[:status_code] = response.status
|
32
|
+
rescue
|
33
|
+
end
|
34
|
+
|
35
|
+
response
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "typhoeus"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
|
8
|
+
module HTTPInstrumentation
|
9
|
+
module Instrumentation
|
10
|
+
# This module is responsible for instrumenting the typhoeus gem.
|
11
|
+
module TyphoeusHook
|
12
|
+
class << self
|
13
|
+
def instrument!
|
14
|
+
Instrumentation.instrument!(::Typhoeus::Request, Easy) if defined?(::Typhoeus::Request)
|
15
|
+
Instrumentation.instrument!(::Typhoeus::Hydra, Multi) if defined?(::Typhoeus::Hydra)
|
16
|
+
end
|
17
|
+
|
18
|
+
def installed?
|
19
|
+
!!(
|
20
|
+
defined?(::Typhoeus::Request) && ::Typhoeus::Request.include?(Easy) &&
|
21
|
+
defined?(::Typhoeus::Hydra) && ::Typhoeus::Hydra.include?(Multi)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Multi
|
27
|
+
def run(*)
|
28
|
+
HTTPInstrumentation.instrument("typhoeus") do |payload|
|
29
|
+
begin
|
30
|
+
payload[:count] = queued_requests.size
|
31
|
+
rescue
|
32
|
+
end
|
33
|
+
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module Easy
|
40
|
+
def run(*)
|
41
|
+
HTTPInstrumentation.instrument("typhoeus") do |payload|
|
42
|
+
retval = super
|
43
|
+
|
44
|
+
begin
|
45
|
+
payload[:http_method] = options[:method]
|
46
|
+
payload[:url] = url
|
47
|
+
payload[:status_code] = response&.response_code
|
48
|
+
rescue
|
49
|
+
end
|
50
|
+
|
51
|
+
retval
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "instrumentation/curb_hook"
|
4
|
+
require_relative "instrumentation/ethon_hook"
|
5
|
+
require_relative "instrumentation/excon_hook"
|
6
|
+
require_relative "instrumentation/httpclient_hook"
|
7
|
+
require_relative "instrumentation/http_hook"
|
8
|
+
require_relative "instrumentation/httpx_hook"
|
9
|
+
require_relative "instrumentation/net_http_hook"
|
10
|
+
require_relative "instrumentation/patron_hook"
|
11
|
+
require_relative "instrumentation/typhoeus_hook"
|
12
|
+
|
13
|
+
module HTTPInstrumentation
|
14
|
+
module Instrumentation
|
15
|
+
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)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/notifications"
|
4
|
+
|
5
|
+
require_relative "http_instrumentation/instrumentation"
|
6
|
+
|
7
|
+
module HTTPInstrumentation
|
8
|
+
IMPLEMENTATIONS = [
|
9
|
+
:curb,
|
10
|
+
:ethon,
|
11
|
+
:excon,
|
12
|
+
:http,
|
13
|
+
:httpclient,
|
14
|
+
:httpx,
|
15
|
+
:net_http,
|
16
|
+
:net_http2,
|
17
|
+
:patron,
|
18
|
+
:typhoeus
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
EVENT = "request.http"
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Add instrumentation into HTTP client libraries. By default all supported
|
25
|
+
# libraries are instrumented. You can pass the only or except options
|
26
|
+
# to limit the instrumentation to a subset of libraries.
|
27
|
+
#
|
28
|
+
# @param only [Array<Symbol>] List of libraries to instrument.
|
29
|
+
# @param except [Array<Symbol>] List of libraries to not instrument.
|
30
|
+
# @return [void]
|
31
|
+
def initialize!(only: nil, except: nil)
|
32
|
+
list = (only || IMPLEMENTATIONS)
|
33
|
+
list &= Array(except) if except
|
34
|
+
|
35
|
+
Instrumentation::CurbHook.instrument! if list.include?(:curb)
|
36
|
+
Instrumentation::EthonHook.instrument! if list.include?(:ethon)
|
37
|
+
Instrumentation::ExconHook.instrument! if list.include?(:excon)
|
38
|
+
Instrumentation::HTTPHook.instrument! if list.include?(:http)
|
39
|
+
Instrumentation::HTTPClientHook.instrument! if list.include?(:httpclient)
|
40
|
+
Instrumentation::HTTPXHook.instrument! if list.include?(:httpx)
|
41
|
+
Instrumentation::NetHTTPHook.instrument! if list.include?(:net_http)
|
42
|
+
Instrumentation::PatronHook.instrument! if list.include?(:patron)
|
43
|
+
Instrumentation::TyphoeusHook.instrument! if list.include?(:typhoeus)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Silence instrumentation for the duration of the block.
|
47
|
+
#
|
48
|
+
# @return [Object] the return value of the block
|
49
|
+
def silence(&block)
|
50
|
+
save_val = Thread.current[:http_instrumentation_silence]
|
51
|
+
begin
|
52
|
+
Thread.current[:http_instrumentation_silence] = true
|
53
|
+
yield
|
54
|
+
ensure
|
55
|
+
Thread.current[:http_instrumentation_silence] = save_val
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# If a block is given, then set the HTTP client name for the duration of the
|
60
|
+
# block. If no block is given, then return the current HTTP client name.
|
61
|
+
#
|
62
|
+
# @param name [String, Symbol, nil] The name of the client to set
|
63
|
+
# @return [String, Symbol, nil] The current name of the client
|
64
|
+
def client(name = nil)
|
65
|
+
save_val = Thread.current[:http_instrumentation_client]
|
66
|
+
if block_given?
|
67
|
+
begin
|
68
|
+
Thread.current[:http_instrumentation_client] = name
|
69
|
+
yield
|
70
|
+
ensure
|
71
|
+
Thread.current[:http_instrumentation_client] = save_val
|
72
|
+
end
|
73
|
+
end
|
74
|
+
save_val
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns true if instrumentation is currently silenced.
|
78
|
+
#
|
79
|
+
# @return [Boolean]
|
80
|
+
def silenced?
|
81
|
+
!!Thread.current[:http_instrumentation_silence]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Instrument the given block with the given client name. An ActiveSupport event will be
|
85
|
+
# fired with the following payload:
|
86
|
+
#
|
87
|
+
# * `:client` - The name of the client as a string.
|
88
|
+
# * `:http_method` - The HTTP method as a lowercase symbol.
|
89
|
+
# * `:url` - The URL as a string.
|
90
|
+
# * `:uri` - The URL as a URI object.
|
91
|
+
# * `:status_code` - The HTTP status code as an integer.
|
92
|
+
# * `:count` - The number of requests made as an integer.
|
93
|
+
#
|
94
|
+
# @param client [String, Symbol] The name of the client.
|
95
|
+
# @return [Object] the return value of the block
|
96
|
+
def instrument(client, &block)
|
97
|
+
payload = {client: (self.client || client).to_s}
|
98
|
+
|
99
|
+
return yield(payload) if silenced?
|
100
|
+
|
101
|
+
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
|
115
|
+
|
116
|
+
payload[:status_code] = payload[:status_code]&.to_i if payload.include?(:status_code)
|
117
|
+
|
118
|
+
payload[:count] = (payload.include?(:count) ? payload[:count].to_i : 1)
|
119
|
+
|
120
|
+
retval
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# Turn the given value into a lowercase symbol.
|
127
|
+
def normalize_http_method(method)
|
128
|
+
return nil if method.nil?
|
129
|
+
method.to_s.downcase.to_sym
|
130
|
+
end
|
131
|
+
|
132
|
+
# Remove any sensitive information from the given URL. Also normalizes
|
133
|
+
# the host and protocol by downcasing them.
|
134
|
+
#
|
135
|
+
# @param url [URI] the sanitized URL
|
136
|
+
def sanitized_uri(url)
|
137
|
+
return nil if url.nil?
|
138
|
+
|
139
|
+
begin
|
140
|
+
uri = URI(url.to_s)
|
141
|
+
rescue URI::Error
|
142
|
+
return nil
|
143
|
+
end
|
144
|
+
|
145
|
+
uri.password = nil
|
146
|
+
uri.user = nil
|
147
|
+
uri.host = uri.host&.downcase
|
148
|
+
|
149
|
+
if uri.respond_to?(:query=) && uri.query
|
150
|
+
params = nil
|
151
|
+
begin
|
152
|
+
params = URI.decode_www_form(uri.query)
|
153
|
+
rescue
|
154
|
+
params = {}
|
155
|
+
end
|
156
|
+
params.reject! { |name, value| name == "access_token" }
|
157
|
+
uri.query = (params.empty? ? nil : URI.encode_www_form(params))
|
158
|
+
end
|
159
|
+
|
160
|
+
uri
|
161
|
+
end
|
162
|
+
|
163
|
+
def uri_without_query_string(uri)
|
164
|
+
URI("#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}").to_s
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
HTTPInstrumentation.initialize!
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: http_instrumentation
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Durand
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-07-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- bbdurand@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- CHANGELOG.md
|
49
|
+
- MIT-LICENSE.txt
|
50
|
+
- README.md
|
51
|
+
- VERSION
|
52
|
+
- http_instrumentation.gemspec
|
53
|
+
- lib/http_instrumentation.rb
|
54
|
+
- lib/http_instrumentation/instrumentation.rb
|
55
|
+
- lib/http_instrumentation/instrumentation/curb_hook.rb
|
56
|
+
- lib/http_instrumentation/instrumentation/ethon_hook.rb
|
57
|
+
- lib/http_instrumentation/instrumentation/excon_hook.rb
|
58
|
+
- lib/http_instrumentation/instrumentation/http_hook.rb
|
59
|
+
- lib/http_instrumentation/instrumentation/httpclient_hook.rb
|
60
|
+
- lib/http_instrumentation/instrumentation/httpx_hook.rb
|
61
|
+
- lib/http_instrumentation/instrumentation/net_http_hook.rb
|
62
|
+
- lib/http_instrumentation/instrumentation/patron_hook.rb
|
63
|
+
- lib/http_instrumentation/instrumentation/typhoeus_hook.rb
|
64
|
+
homepage: https://github.com/bdurand/http_instrumentation
|
65
|
+
licenses:
|
66
|
+
- MIT
|
67
|
+
metadata: {}
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '2.5'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubygems_version: 3.4.12
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: ActiveSupoprt instrumentation for a variety of Ruby HTTP client libraries.
|
87
|
+
test_files: []
|