lib/wcc/contentful/simple_client.rb in wcc-contentful-0.4.0.pre.rc vs lib/wcc/contentful/simple_client.rb in wcc-contentful-1.0.0.pre.rc1
- old
+ new
@@ -1,9 +1,10 @@
# frozen_string_literal: true
require_relative 'simple_client/response'
require_relative 'simple_client/management'
+require_relative 'instrumentation'
module WCC::Contentful
# The SimpleClient accesses the Contentful CDN to get JSON responses,
# returning the raw JSON data as a parsed hash. This is the bottom layer of
# the WCC::Contentful gem.
@@ -14,39 +15,47 @@
#
# It can be configured to access any API url and exposes only a single method,
# `get`. This method returns a WCC::Contentful::SimpleClient::Response
# that handles paging automatically.
#
- # The SimpleClient by default uses 'http' to perform the gets, but any HTTP
- # client can be injected by passing a proc as the `adapter:` option.
+ # The SimpleClient by default uses 'faraday' to perform the gets, but any HTTP
+ # client adapter be injected by passing the `connection:` option.
#
# @api Client
class SimpleClient
+ include WCC::Contentful::Instrumentation
+
attr_reader :api_url
attr_reader :space
# Creates a new SimpleClient with the given configuration.
#
# @param [String] api_url the base URL of the Contentful API to connect to
# @param [String] space The Space ID to access
# @param [String] access_token A Contentful Access Token to be sent in the Authorization header
# @param [Hash] options The remaining optional parameters, defined below
- # @option options [Symbol, Object] adapter The Adapter to use to make requests.
+ # @option options [Symbol, Object] connection The Faraday connection to use to make requests.
# Auto-discovered based on what gems are installed if this is not provided.
# @option options [String] default_locale The locale query param to set by default.
# @option options [String] environment The contentful environment to access. Defaults to 'master'.
# @option options [Boolean] no_follow_redirects If true, do not follow 300 level redirects.
+ # @option options [Number] rate_limit_wait_timeout The maximum time to block the thread waiting
+ # on a rate limit response. By default will wait for one 429 and then fail on the second 429.
def initialize(api_url:, space:, access_token:, **options)
@api_url = URI.join(api_url, '/spaces/', space + '/')
@space = space
@access_token = access_token
- @adapter = SimpleClient.load_adapter(options[:adapter])
+ @adapter = SimpleClient.load_adapter(options[:connection])
@options = options
+ @_instrumentation = @options[:instrumentation]
@query_defaults = {}
@query_defaults[:locale] = @options[:default_locale] if @options[:default_locale]
+ # default 1.5 so that we retry one time then fail if still rate limited
+ # https://www.contentful.com/developers/docs/references/content-preview-api/#/introduction/api-rate-limits
+ @rate_limit_wait_timeout = @options[:rate_limit_wait_timeout] || 1.5
return unless options[:environment].present?
@api_url = URI.join(@api_url, 'environments/', options[:environment] + '/')
end
@@ -55,17 +64,21 @@
# space and environment. Query parameters are merged with the defaults and
# appended to the request.
def get(path, query = {})
url = URI.join(@api_url, path)
+ resp =
+ _instrument 'get_http', url: url, query: query do
+ get_http(url, query)
+ end
Response.new(self,
{ url: url, query: query },
- get_http(url, query))
+ resp)
end
ADAPTERS = {
- http: ['http', '> 1.0', '< 3.0'],
+ faraday: ['faraday', '>= 0.9'],
typhoeus: ['typhoeus', '~> 1.0']
}.freeze
def self.load_adapter(adapter)
case adapter
@@ -78,41 +91,65 @@
next
end
end
raise ArgumentError, 'Unable to load adapter! Please install one of '\
"#{ADAPTERS.values.map(&:join).join(',')}"
- when :http
- require_relative 'simple_client/http_adapter'
- HttpAdapter.new
+ when :faraday
+ require 'faraday'
+ ::Faraday.new do |faraday|
+ faraday.response :logger, (Rails.logger if defined?(Rails)), { headers: false, bodies: false }
+ faraday.adapter :net_http
+ end
when :typhoeus
require_relative 'simple_client/typhoeus_adapter'
TyphoeusAdapter.new
else
- unless adapter.respond_to?(:call)
+ unless adapter.respond_to?(:get)
raise ArgumentError, "Adapter #{adapter} is not invokeable! Please "\
- "pass a proc or use one of #{ADAPTERS.keys}"
+ "pass use one of #{ADAPTERS.keys} or create a Faraday-compatible adapter"
end
adapter
end
end
private
- def get_http(url, query, headers = {}, proxy = {})
+ def _instrumentation_event_prefix
+ # Unify all CDN, Management, Preview notifications under same namespace
+ '.simpleclient.contentful.wcc'
+ end
+
+ def get_http(url, query, headers = {})
headers = {
Authorization: "Bearer #{@access_token}"
}.merge(headers || {})
q = @query_defaults.dup
q = q.merge(query) if query
- resp = @adapter.call(url, q, headers, proxy)
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ loop do
+ resp = @adapter.get(url, q, headers)
- if [301, 302, 307].include?(resp.code) && !@options[:no_follow_redirects]
- resp = get_http(resp.headers['location'], nil, headers, proxy)
+ if [301, 302, 307].include?(resp.status) && !@options[:no_follow_redirects]
+ url = resp.headers['Location']
+ next
+ end
+
+ if resp.status == 429 &&
+ reset = resp.headers['X-Contentful-RateLimit-Reset'].presence
+ reset = reset.to_f
+ _instrument 'rate_limit', start: start, reset: reset, timeout: @rate_limit_wait_timeout
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ if (now - start) + reset < @rate_limit_wait_timeout
+ sleep(reset)
+ next
+ end
+ end
+
+ return resp
end
- resp
end
# The CDN SimpleClient accesses 'https://cdn.contentful.com' to get raw
# JSON responses. It exposes methods to query entries, assets, and content_types.
# The responses are instances of WCC::Contentful::SimpleClient::Response
@@ -133,35 +170,50 @@
'cdn'
end
# Gets an entry by ID
def entry(key, query = {})
- resp = get("entries/#{key}", query)
+ resp =
+ _instrument 'entries', id: key, type: 'Entry', query: query do
+ get("entries/#{key}", query)
+ end
resp.assert_ok!
end
# Queries entries with optional query parameters
def entries(query = {})
- resp = get('entries', query)
+ resp =
+ _instrument 'entries', type: 'Entry', query: query do
+ get('entries', query)
+ end
resp.assert_ok!
end
# Gets an asset by ID
def asset(key, query = {})
- resp = get("assets/#{key}", query)
+ resp =
+ _instrument 'entries', type: 'Asset', id: key, query: query do
+ get("assets/#{key}", query)
+ end
resp.assert_ok!
end
# Queries assets with optional query parameters
def assets(query = {})
- resp = get('assets', query)
+ resp =
+ _instrument 'entries', type: 'Asset', query: query do
+ get('assets', query)
+ end
resp.assert_ok!
end
# Queries content types with optional query parameters
def content_types(query = {})
- resp = get('content_types', query)
+ resp =
+ _instrument 'content_types', query: query do
+ get('content_types', query)
+ end
resp.assert_ok!
end
# Accesses the Sync API to get a list of items that have changed since
# the last sync.
@@ -175,22 +227,26 @@
{ sync_token: sync_token }
else
{ initial: true }
end
query = query.merge(sync_token)
- resp = SyncResponse.new(get('sync', query))
+ resp =
+ _instrument 'sync', sync_token: sync_token, query: query do
+ get('sync', query)
+ end
+ resp = SyncResponse.new(resp)
resp.assert_ok!
end
end
# @api Client
class Preview < Cdn
def initialize(space:, preview_token:, **options)
super(
- api_url: options[:api_url] || 'https://preview.contentful.com/',
+ **options,
+ api_url: options[:preview_api_url] || 'https://preview.contentful.com/',
space: space,
- access_token: preview_token,
- **options
+ access_token: preview_token
)
end
def client_type
'preview'