Skip to content

Commit 10af0f4

Browse files
committed
instrumentation support
- Add check for valid instrumentation config - Pass instrumentation and logger explicitly into middleware - Wrap instrumentation calls in begin-rescue - Remove required activesupport dependency - Add fake for testing instrumentation - Maintain single-retry behavior
1 parent d473fbd commit 10af0f4

File tree

11 files changed

+239
-170
lines changed

11 files changed

+239
-170
lines changed

Gemfile.lock

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ PATH
22
remote: .
33
specs:
44
zendesk_api (3.1.1)
5-
activesupport
65
base64
76
faraday (> 2.0.0)
87
faraday-multipart

lib/zendesk_api/client.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def initialize
9797
@resource_cache = {}
9898

9999
check_url
100+
check_instrumentation
100101

101102
config.retry = !!config.retry # nil -> false
102103

@@ -167,7 +168,7 @@ def build_connection
167168
Faraday.new(config.options) do |builder|
168169
# response
169170
builder.use ZendeskAPI::Middleware::Response::RaiseError
170-
builder.use ZendeskAPI::Middleware::Response::ZendeskRequestEvent, self if config.instrumentation.respond_to?(:instrument)
171+
builder.use ZendeskAPI::Middleware::Response::ZendeskRequestEvent, instrumentation: config.instrumentation, logger: config.logger if config.instrumentation
171172
builder.use ZendeskAPI::Middleware::Response::Callback, self
172173
builder.use ZendeskAPI::Middleware::Response::Logger, config.logger if config.logger
173174
builder.use ZendeskAPI::Middleware::Response::ParseIsoDates
@@ -183,7 +184,7 @@ def build_connection
183184
set_authentication(builder, config)
184185

185186
if config.cache
186-
builder.use ZendeskAPI::Middleware::Request::EtagCache, {cache: config.cache, instrumentation: config.instrumentation}
187+
builder.use ZendeskAPI::Middleware::Request::EtagCache, cache: config.cache, instrumentation: config.instrumentation
187188
end
188189

189190
builder.use ZendeskAPI::Middleware::Request::Upload
@@ -220,6 +221,14 @@ def check_url
220221
end
221222
end
222223

224+
def check_instrumentation
225+
return unless config.instrumentation
226+
227+
unless config.instrumentation.respond_to?(:instrument)
228+
raise ArgumentError, "instrumentation must respond to #instrument"
229+
end
230+
end
231+
223232
def set_raise_error_when_rated_limited
224233
config.raise_error_when_rate_limited = if config.retry
225234
false

lib/zendesk_api/configuration.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Configuration
5454
# specify if you want a (network layer) exception to elicit a retry
5555
attr_accessor :retry_on_exception
5656

57-
# specify if you wnat instrumentation to be used
57+
# specify if you want instrumentation to be used
5858
attr_accessor :instrumentation
5959

6060
def initialize

lib/zendesk_api/middleware/request/etag_cache.rb

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
require "faraday/middleware"
2-
require "active_support/notifications"
32

43
module ZendeskAPI
54
module Middleware
@@ -43,18 +42,30 @@ def call(environment)
4342
content_length: cached[:response_headers][:content_length],
4443
content_encoding: cached[:response_headers][:content_encoding]
4544
)
46-
@instrumentation&.instrument("zendesk.cache_hit",
47-
{
48-
endpoint: env[:url].path,
49-
status: env[:status]
50-
})
45+
if @instrumentation
46+
begin
47+
@instrumentation.instrument("zendesk.cache_hit",
48+
{
49+
endpoint: env[:url]&.path,
50+
status: env[:status]
51+
})
52+
rescue
53+
# Swallow instrumentation errors to maintain cache behavior
54+
end
55+
end
5156
elsif env[:status] == 200 && env[:response_headers]["Etag"] # modified and cacheable
5257
@cache.write(cache_key(env), env.to_hash)
53-
@instrumentation&.instrument("zendesk.cache_miss",
54-
{
55-
endpoint: env[:url].path,
56-
status: env[:status]
57-
})
58+
if @instrumentation
59+
begin
60+
@instrumentation.instrument("zendesk.cache_miss",
61+
{
62+
endpoint: env[:url]&.path,
63+
status: env[:status]
64+
})
65+
rescue
66+
# Swallow instrumentation errors to maintain cache behavior
67+
end
68+
end
5869
end
5970
end
6071
end

lib/zendesk_api/middleware/request/retry.rb

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def initialize(app, options = {})
2121
def call(env)
2222
# Duplicate env for retries but keep attempt counter persistent
2323
original_env = env.dup
24-
original_env[:call_attempt] ||= 0
24+
original_env[:call_attempt] = (env[:call_attempt] || 0)
2525

2626
exception_happened = false
2727
response = nil
@@ -47,8 +47,8 @@ def call(env)
4747
return @app.call(original_env)
4848
end
4949

50-
# Loop through consecutive retryable responses (e.g., multiple 429s)
51-
while response && @error_codes.include?(response.env[:status])
50+
# Retry once if response has a retryable error code
51+
if response && @error_codes.include?(response.env[:status])
5252
original_env[:call_attempt] += 1
5353
seconds_left = (response.env[:response_headers][:retry_after] || DEFAULT_RETRY_AFTER).to_i
5454
@logger&.warn "You may have been rate limited. Retrying in #{seconds_left} seconds..."
@@ -64,21 +64,26 @@ def call(env)
6464

6565
def instrument_retry(env, reason, delay)
6666
return unless @instrumentation
67-
@instrumentation.instrument(
68-
"zendesk.retry",
69-
{
70-
attempt: env[:call_attempt],
71-
endpoint: env[:url].path,
72-
method: env[:method],
73-
reason: reason,
74-
delay: delay
75-
}
76-
)
67+
68+
begin
69+
@instrumentation.instrument(
70+
"zendesk.retry",
71+
{
72+
attempt: env[:call_attempt],
73+
endpoint: env[:url]&.path,
74+
method: env[:method],
75+
reason: reason,
76+
delay: delay
77+
}
78+
)
79+
rescue => e
80+
@logger&.debug("zendesk.retry instrumentation failed: #{e.message}")
81+
end
7782
end
7883

7984
def sleep_with_logging(seconds_left)
8085
seconds_left.times do |i|
81-
sleep 1 if seconds_left > 0
86+
sleep 1
8287
time_left = seconds_left - i
8388
@logger&.warn "#{time_left}..." if time_left > 0 && time_left % 5 == 0
8489
end

lib/zendesk_api/middleware/response/zendesk_request_event.rb

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,50 @@ module Middleware
55
module Response
66
# @private
77
class ZendeskRequestEvent < Faraday::Middleware
8-
def initialize(app, client)
8+
def initialize(app, options = {})
99
super(app)
10-
@client = client
10+
@instrumentation = options[:instrumentation]
11+
@logger = options[:logger]
1112
end
1213

1314
def call(env)
1415
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1516
@app.call(env).on_complete do |response_env|
1617
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1718
duration = (end_time - start_time) * 1000.0
18-
instrumentation = @client.config.instrumentation
19-
if instrumentation
20-
instrumentation.instrument("zendesk.request",
21-
{duration: duration,
22-
endpoint: response_env[:url].path,
23-
method: response_env[:method],
24-
status: response_env[:status]})
25-
if response_env[:status] < 500
26-
instrumentation.instrument("zendesk.rate_limit",
27-
{
28-
endpoint: response_env[:url].path,
29-
status: response_env[:status],
30-
threshold: response_env[:response_headers] ? response_env[:response_headers]["X-Rate-Limit-Remaining"] : nil,
31-
limit: response_env[:response_headers] ? response_env[:response_headers]["X-Rate-Limit"] : nil,
32-
reset: response_env[:response_headers] ? response_env[:response_headers]["X-Rate-Limit-Reset"] : nil
33-
})
19+
if @instrumentation
20+
begin
21+
@instrumentation.instrument("zendesk.request",
22+
{duration: duration,
23+
endpoint: response_env[:url]&.path,
24+
method: response_env[:method],
25+
status: response_env[:status]})
26+
rescue => e
27+
@logger&.debug("zendesk.request instrumentation failed: #{e.message}")
28+
end
29+
30+
if response_env[:status] && response_env[:status] < 500
31+
headers = response_env[:response_headers]
32+
if headers
33+
remaining = headers["X-Rate-Limit-Remaining"]
34+
limit = headers["X-Rate-Limit"]
35+
reset = headers["X-Rate-Limit-Reset"]
36+
37+
if remaining || limit || reset
38+
begin
39+
@instrumentation.instrument("zendesk.rate_limit",
40+
{
41+
endpoint: response_env[:url]&.path,
42+
status: response_env[:status],
43+
remaining: remaining,
44+
limit: limit,
45+
reset: reset
46+
})
47+
rescue => e
48+
@logger&.debug("zendesk.rate_limit instrumentation failed: #{e.message}")
49+
end
50+
end
51+
end
3452
end
3553
end
3654
end
Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
require "core/spec_helper"
2-
require "active_support/cache"
32

43
describe ZendeskAPI::Middleware::Request::EtagCache do
54
it "caches" do
@@ -21,60 +20,61 @@
2120
end
2221

2322
context "instrumentation" do
24-
let(:instrumentation) { double("Instrumentation") }
25-
let(:cache) { ActiveSupport::Cache::MemoryStore.new }
26-
let(:status) { nil }
23+
let(:instrumenter) { TestInstrumenter.new }
24+
let(:cache) { ZendeskAPI::LRUCache.new(5) }
2725
let(:middleware) do
2826
ZendeskAPI::Middleware::Request::EtagCache.new(
2927
->(env) { Faraday::Response.new(env) },
3028
cache: cache,
31-
instrumentation: instrumentation
29+
instrumentation: instrumenter
3230
)
3331
end
3432
let(:env) do
3533
{
3634
url: URI("https://example.zendesk.com/api/v2/blergh"),
3735
method: :get,
3836
request_headers: {},
39-
response_headers: {"Etag" => "x", :x_rate_limit_remaining => 10},
40-
status: status,
37+
response_headers: {"Etag" => "x"},
38+
status: nil,
4139
body: {"x" => 1},
4240
response_body: {"x" => 1}
4341
}
4442
end
45-
let(:no_instrumentation_middleware) do
46-
ZendeskAPI::Middleware::Request::EtagCache.new(
47-
->(env) { Faraday::Response.new(env) },
48-
cache: cache,
49-
instrumentation: nil
50-
)
51-
end
52-
before do
53-
allow(instrumentation).to receive(:instrument)
54-
end
5543

56-
it "emits cache_miss on first request" do
57-
expect(instrumentation).to receive(:instrument).with(
58-
"zendesk.cache_miss",
59-
hash_including(endpoint: "/api/v2/blergh", status: 200)
60-
)
44+
it "instruments cache miss on first request" do
6145
env[:status] = 200
62-
middleware.call(env).on_complete { |_e| 1 }
63-
end
46+
middleware.call(env).on_complete { |_e| }
6447

65-
it "don't care on no instrumentation" do
66-
env[:status] = 200
67-
no_instrumentation_middleware.call(env).on_complete { |_e| 1 }
48+
cache_events = instrumenter.find_events("zendesk.cache_miss")
49+
expect(cache_events.size).to eq(1)
50+
51+
event = cache_events.first[:payload]
52+
expect(event[:endpoint]).to eq("/api/v2/blergh")
53+
expect(event[:status]).to eq(200)
6854
end
6955

70-
it "emits cache_hit on 304 response" do
56+
it "instruments cache hit on 304 response" do
7157
cache.write(middleware.cache_key(env), env)
72-
expect(instrumentation).to receive(:instrument).with(
73-
"zendesk.cache_hit",
74-
hash_including(endpoint: "/api/v2/blergh", status: 304)
75-
)
7658
env[:status] = 304
77-
middleware.call(env).on_complete { |_e| 1 }
59+
middleware.call(env).on_complete { |_e| }
60+
61+
cache_events = instrumenter.find_events("zendesk.cache_hit")
62+
expect(cache_events.size).to eq(1)
63+
64+
event = cache_events.first[:payload]
65+
expect(event[:endpoint]).to eq("/api/v2/blergh")
66+
expect(event[:status]).to eq(304)
67+
end
68+
69+
it "does not crash when instrumentation is nil" do
70+
no_instrumentation_middleware = ZendeskAPI::Middleware::Request::EtagCache.new(
71+
->(env) { Faraday::Response.new(env) },
72+
cache: cache,
73+
instrumentation: nil
74+
)
75+
76+
env[:status] = 200
77+
expect { no_instrumentation_middleware.call(env).on_complete { |_e| } }.not_to raise_error
7878
end
7979
end
8080
end

0 commit comments

Comments
 (0)