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 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: []