HTTP Instrumentation

Continuous Integration Ruby Style Guide

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).

Supported Libraries

Note that several other popular HTTP client libraries like Faraday, HTTParty, and RestClient are built on top of these low level libraries.

Usage

To capture information about HTTP requests, simply subscribe to the request.http events with ActiveSupport notifications (note that you should really use monotonic_subscribe instead of subscribe to avoid issues with clock adjustments).

The payload on event notifications for all HTTP requests will include:

  • :client - The client library used to make the request
  • :count - The number of HTTP requests that were made

If a single HTTP request was made, then these keys will exist as well:

  • :uri - The URI for the request
  • :url - The URL for the request with any query string stripped off
  • :http_method - The HTTP method for the request
  • :status_code - The numeric HTTP status code for the response

These additional values will not be present if multiple, concurrent requests were made. Only the typhoeus, ethon, and httpx libraries support making concurrent requests.

ActiveSupport::Notifications.monotonic_subscribe("request.http") do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  client = event.payload[:client]
  count = event.payload[:count]
  url = event.payload[:url]
  uri = event.payload[:uri]
  http_method = event.payload[:http_method]
  status_code = event.payload[:status_code]

  puts "HTTP request: client: #{client}, count: #{count}, duration: #{event.duration}ms"
  if count == 1
    puts "#{http_method} #{url} - status: #{status_code}, host: #{uri&.host}"
  end
end

# Single request
Net::HTTP.get(URI("https://example.com/info"))
# => HTTP request: client: net/http, count: 1, duration: 100ms
# => GET https://example.com/info - status 200, host: example.com

# Multiple, concurrent requests
HTTPX.get("https://example.com/r1", "https://example.com/r2")
# => HTTP request: client: httpx, count: 2, duration: 150ms

Security

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.

The :url element will also have the query string stripped from it so it will just include the scheme, host, and path.

HTTP.get("https://user@[email protected]/path")
HTTP.get("https://example.com/path?access_token=secrettoken")
# event.payload[:url] will be https://example.com/path in both cases

The hostname will also be converted to lowercase in these attributes.

Silencing Notifications

If you want to suppress notifications, you can do so by surrounding code with an HTTPInstrumentation.silence block.

HTTPInstrumentation.silence do
  HTTP.get("https://example.com/info") # Notification will not be sent
end

Custom HTTP Clients

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.

class MyHttpClient
  def get(url)
    HTTPInstrumentation.instrument("my_client") do |payload|
      response = Net::HTTP.get(URI(url))

      payload[:http_method] = :get
      payload[:url] = url
      payload[:status_code] = response.code

      response
    end
  end
end

MyHttpClient.get("https://example.com/")
# Event => {client: "my_client", http_method => :get, url: "https://example.com/"}

You can also take advantage of the existing instrumentation and just override the client name in the notification event.

class MyHttpClient
  def get(url)
    HTTPInstrumentation.client("my_client")
      Net::HTTP.get(URI(url))
    end
  end
end

MyHttpClient.get("https://example.com/")
# Event => {client: "my_client", http_method => :get, url: "https://example.com/"}

Installation

Add this line to your application's Gemfile:

gem "http_instrumentation"

Then execute:

$ bundle

Or install it yourself as:

$ gem install http_instrumentation

Contributing

Open a pull request on GitHub.

Please use the standardrb syntax and lint your code with standardrb --fix before submitting.

License

The gem is available as open source under the terms of the MIT License.