http_instrumentation 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []