http_instrumentation 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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
|
+
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](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: []
|