Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ PATH
remote: .
specs:
zendesk_api (3.1.1)
activesupport
faraday (> 2.0.0)
faraday-multipart
hashie (>= 3.5.2)
Expand Down
7 changes: 5 additions & 2 deletions lib/zendesk_api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require 'zendesk_api/middleware/request/encode_json'
require 'zendesk_api/middleware/request/url_based_access_token'
require 'zendesk_api/middleware/response/callback'
require 'zendesk_api/middleware/response/zendesk_request_event'
require 'zendesk_api/middleware/response/deflate'
require 'zendesk_api/middleware/response/gzip'
require 'zendesk_api/middleware/response/sanitize_response'
Expand Down Expand Up @@ -146,6 +147,7 @@ def build_connection
Faraday.new(config.options) do |builder|
# response
builder.use ZendeskAPI::Middleware::Response::RaiseError
builder.use ZendeskAPI::Middleware::Response::ZendeskRequestEvent, self if config.instrumentation.respond_to?(:instrument)
builder.use ZendeskAPI::Middleware::Response::Callback, self
builder.use ZendeskAPI::Middleware::Response::Logger, config.logger if config.logger
builder.use ZendeskAPI::Middleware::Response::ParseIsoDates
Expand All @@ -161,7 +163,7 @@ def build_connection
set_authentication(builder, config)

if config.cache
builder.use ZendeskAPI::Middleware::Request::EtagCache, :cache => config.cache
builder.use ZendeskAPI::Middleware::Request::EtagCache, { :cache => config.cache, :instrumentation => config.instrumentation }
end

builder.use ZendeskAPI::Middleware::Request::Upload
Expand All @@ -173,7 +175,8 @@ def build_connection
builder.use ZendeskAPI::Middleware::Request::Retry,
:logger => config.logger,
:retry_codes => config.retry_codes,
:retry_on_exception => config.retry_on_exception
:retry_on_exception => config.retry_on_exception,
:instrumentation => config.instrumentation
end
if config.raise_error_when_rate_limited
builder.use ZendeskAPI::Middleware::Request::RaiseRateLimited, :logger => config.logger
Expand Down
3 changes: 3 additions & 0 deletions lib/zendesk_api/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ class Configuration
# specify if you want a (network layer) exception to elicit a retry
attr_accessor :retry_on_exception

# specify if you wnat instrumentation to be used
attr_accessor :instrumentation

def initialize
@client_options = {}
@use_resource_cache = true
Expand Down
12 changes: 12 additions & 0 deletions lib/zendesk_api/middleware/request/etag_cache.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "faraday/middleware"
require 'active_support/notifications'

module ZendeskAPI
module Middleware
Expand All @@ -9,6 +10,7 @@ module Request
class EtagCache < Faraday::Middleware
def initialize(app, options = {})
@app = app
@instrumentation = options[:instrumentation] if options[:instrumentation].respond_to?(:instrument)
@cache = options[:cache] ||
raise("need :cache option e.g. ActiveSupport::Cache::MemoryStore.new")
@cache_key_prefix = options.fetch(:cache_key_prefix, :faraday_etags)
Expand Down Expand Up @@ -41,8 +43,18 @@ def call(environment)
:content_length => cached[:response_headers][:content_length],
:content_encoding => cached[:response_headers][:content_encoding]
)
@instrumentation&.instrument("zendesk.cache_hit",
{
endpoint: env[:url].path,
status: env[:status]
})
elsif env[:status] == 200 && env[:response_headers]["Etag"] # modified and cacheable
@cache.write(cache_key(env), env.to_hash)
@instrumentation&.instrument("zendesk.cache_miss",
{
endpoint: env[:url].path,
status: env[:status]
})
end
end
end
Expand Down
16 changes: 16 additions & 0 deletions lib/zendesk_api/middleware/request/retry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ def initialize(app, options = {})
@logger = options[:logger]
@error_codes = options.key?(:retry_codes) && options[:retry_codes] ? options[:retry_codes] : DEFAULT_ERROR_CODES
@retry_on_exception = options.key?(:retry_on_exception) && options[:retry_on_exception] ? options[:retry_on_exception] : false
@instrumentation = options[:instrumentation]
end

def call(env)
original_env = env.dup
if original_env[:call_attempt]
original_env[:call_attempt] += 1
else
original_env[:call_attempt] = 1
end
exception_happened = false
if @retry_on_exception
begin
Expand All @@ -40,6 +46,16 @@ def call(env)

@logger.warn "You have been rate limited. Retrying in #{seconds_left} seconds..." if @logger

if @instrumentation
@instrumentation.instrument("zendesk.retry",
{
attempt: original_env[:call_attempt],
endpoint: original_env[:url].path,
method: original_env[:method],
reason: exception_happened ? 'exception' : 'rate_limited',
delay: seconds_left
})
end
seconds_left.times do |i|
sleep 1
time_left = seconds_left - i
Expand Down
41 changes: 41 additions & 0 deletions lib/zendesk_api/middleware/response/zendesk_request_event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require "faraday/response"

module ZendeskAPI
module Middleware
module Response
# @private
class ZendeskRequestEvent < Faraday::Middleware
def initialize(app, client)
super(app)
@client = client
end

def call(env)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@app.call(env).on_complete do |response_env|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
duration = (end_time - start_time) * 1000.0
instrumentation = @client.config.instrumentation
if instrumentation
instrumentation.instrument("zendesk.request",
{ duration: duration,
endpoint: response_env[:url].path,
method: response_env[:method],
status: response_env[:status] })
if response_env[:status] < 500
instrumentation.instrument("zendesk.rate_limit",
{
endpoint: response_env[:url].path,
status: response_env[:status],
threshold: response_env[:response_headers] ? response_env[:response_headers][:x_rate_limit_remaining] : nil,
limit: response_env[:response_headers] ? response_env[:response_headers][:x_rate_limit] : nil,
reset: response_env[:response_headers] ? response_env[:response_headers][:x_rate_limit_reset] : nil
})
end
end
end
end
end
end
end
end
59 changes: 59 additions & 0 deletions spec/core/middleware/request/etag_cache_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'core/spec_helper'
require 'active_support/cache'

describe ZendeskAPI::Middleware::Request::EtagCache do
it "caches" do
Expand All @@ -18,4 +19,62 @@
expect(response.headers[header]).to eq(first_response.headers[header])
end
end

context "instrumentation" do
let(:instrumentation) { double("Instrumentation") }
let(:cache) { ActiveSupport::Cache::MemoryStore.new }
let(:status) { nil }
let(:middleware) do
ZendeskAPI::Middleware::Request::EtagCache.new(
->(env) { Faraday::Response.new(env) },
cache: cache,
instrumentation: instrumentation
)
end
let(:env) do
{
url: URI("https://example.zendesk.com/api/v2/blergh"),
method: :get,
request_headers: {},
response_headers: { "Etag" => "x", x_rate_limit_remaining: 10 },
status: status,
body: { "x" => 1 },
response_body: { "x" => 1 }
}
end
let(:no_instrumentation_middleware) do
ZendeskAPI::Middleware::Request::EtagCache.new(
->(env) { Faraday::Response.new(env) },
cache: cache,
instrumentation: nil
)
end
before do
allow(instrumentation).to receive(:instrument)
end

it "emits cache_miss on first request" do
expect(instrumentation).to receive(:instrument).with(
"zendesk.cache_miss",
hash_including(endpoint: "/api/v2/blergh", status: 200)
)
env[:status] = 200
middleware.call(env).on_complete { |_e| 1 }
end

it "don't care on no instrumentation" do
env[:status] = 200
no_instrumentation_middleware.call(env).on_complete { |_e| 1 }
end

it "emits cache_hit on 304 response" do
cache.write(middleware.cache_key(env), env)
expect(instrumentation).to receive(:instrument).with(
"zendesk.cache_hit",
hash_including(endpoint: "/api/v2/blergh", status: 304)
)
env[:status] = 304
middleware.call(env).on_complete { |_e| 1 }
end
end
end
32 changes: 32 additions & 0 deletions spec/core/middleware/request/retry_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,36 @@ def runtime
end
end
end

context "with instrumentation on retry" do
let(:instrumentation) { double("Instrumentation") }
let(:middleware) do
ZendeskAPI::Middleware::Request::Retry.new(client.connection.builder.app)
end

before do
allow(instrumentation).to receive(:instrument)
client.config.instrumentation = instrumentation
# Inject instrumentation into middleware instance
allow_any_instance_of(ZendeskAPI::Middleware::Request::Retry).to receive(:instrumentation).and_return(instrumentation)
stub_request(:get, %r{instrumented}).to_return(:status => 429, :headers => { :retry_after => 1 }).to_return(:status => 200)
end

it "calls instrumentation on retry" do
expect(instrumentation).to receive(:instrument).with(
"zendesk.retry",
hash_including(:attempt, :endpoint, :method, :reason, :delay)
).at_least(:once)
client.connection.get("instrumented")
end

it "does not call instrumentation when no retry occurs" do
stub_request(:get, %r{no_retry}).to_return(:status => 200)
expect(instrumentation).not_to receive(:instrument).with(
"zendesk.retry",
hash_including(:attempt, :endpoint, :method, :reason, :delay)
)
client.connection.get("no_retry")
end
end
end
86 changes: 86 additions & 0 deletions spec/core/middleware/response/zendesk_request_event_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require_relative '../../../spec_helper'
require 'faraday'
require 'zendesk_api/middleware/response/zendesk_request_event'

RSpec.describe ZendeskAPI::Middleware::Response::ZendeskRequestEvent do
let(:instrumentation) { double('Instrumentation') }
let(:client) do
double('Client', config: double('Config', instrumentation: instrumentation))
end
let(:app) { ->(env) { Faraday::Response.new(env) } }
let(:middleware) { described_class.new(app, client) }
let(:response_headers) do
{
x_rate_limit_remaining: 10,
x_rate_limit: 100,
x_rate_limit_reset: 1234567890
}
end
let(:env) do
{
url: URI('https://example.zendesk.com/api/v2/tickets'),
method: :get,
status: status,
response_headers: response_headers
}
end

before do
allow(instrumentation).to receive(:instrument)
end

context 'when the response status is less than 500' do
let(:status) { 200 }

it 'instruments zendesk.request and zendesk.rate_limit' do
expect(instrumentation).to receive(:instrument).with(
'zendesk.request',
hash_including(:duration, endpoint: '/api/v2/tickets', method: :get, status: 200)
)
expect(instrumentation).to receive(:instrument).with(
'zendesk.rate_limit',
hash_including(endpoint: '/api/v2/tickets', status: 200)
)
middleware.call(env).on_complete { |_response_env| 1 }
end
end

context 'when the response status is 500 or greater' do
let(:status) { 500 }

it 'instruments only zendesk.request' do
expect(instrumentation).to receive(:instrument).with(
'zendesk.request',
hash_including(:duration, endpoint: '/api/v2/tickets', method: :get, status: 500)
)
expect(instrumentation).not_to receive(:instrument).with('zendesk.rate_limit', anything)
middleware.call(env).on_complete { |_response_env| 1 }
end
end

context 'duration calculation' do
let(:status) { 201 }

it 'passes a positive duration to instrumentation' do
expect(instrumentation).to receive(:instrument) do |event, payload|
if event == 'zendesk.request'
expect(payload[:duration]).to be > 0
end
end
expect(instrumentation).to receive(:instrument).with('zendesk.rate_limit', anything)
middleware.call(env).on_complete { |_response_env| 1 }
end
end

context 'when instrumentation is nil' do
let(:status) { 200 }
let(:client) do
double('Client', config: double('Config', instrumentation: nil))
end
let(:middleware) { described_class.new(app, client) }

it 'does not raise an error' do
expect { middleware.call(env).on_complete { |_response_env| 1 } }.not_to raise_error
end
end
end
7 changes: 7 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require 'rspec'

RSpec.configure do |config|
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
1 change: 1 addition & 0 deletions zendesk_api.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ Gem::Specification.new do |s|
s.add_dependency "inflection"
s.add_dependency "multipart-post", "~> 2.0"
s.add_dependency "mini_mime"
s.add_dependency "activesupport"
end
Loading