lhc 13.0.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/rubocop.yml +15 -0
- data/.github/workflows/test.yml +15 -0
- data/.rubocop.yml +344 -19
- data/.ruby-version +1 -1
- data/README.md +44 -0
- data/Rakefile +3 -3
- data/lhc.gemspec +3 -1
- data/lib/lhc.rb +59 -59
- data/lib/lhc/concerns/lhc/fix_invalid_encoding_concern.rb +1 -0
- data/lib/lhc/config.rb +3 -0
- data/lib/lhc/endpoint.rb +3 -0
- data/lib/lhc/error.rb +5 -1
- data/lib/lhc/interceptor.rb +4 -0
- data/lib/lhc/interceptors.rb +1 -0
- data/lib/lhc/interceptors/auth.rb +3 -4
- data/lib/lhc/interceptors/caching.rb +14 -3
- data/lib/lhc/interceptors/logging.rb +2 -0
- data/lib/lhc/interceptors/monitoring.rb +46 -11
- data/lib/lhc/interceptors/retry.rb +2 -0
- data/lib/lhc/interceptors/rollbar.rb +1 -0
- data/lib/lhc/interceptors/throttle.rb +7 -2
- data/lib/lhc/interceptors/zipkin.rb +2 -0
- data/lib/lhc/request.rb +13 -3
- data/lib/lhc/response.rb +1 -0
- data/lib/lhc/response/data.rb +1 -1
- data/lib/lhc/version.rb +1 -1
- data/spec/error/to_s_spec.rb +7 -2
- data/spec/formats/multipart_spec.rb +2 -2
- data/spec/formats/plain_spec.rb +1 -1
- data/spec/interceptors/after_response_spec.rb +1 -1
- data/spec/interceptors/caching/main_spec.rb +2 -2
- data/spec/interceptors/caching/multilevel_cache_spec.rb +2 -1
- data/spec/interceptors/define_spec.rb +1 -0
- data/spec/interceptors/monitoring/caching_spec.rb +66 -0
- data/spec/interceptors/response_competition_spec.rb +2 -2
- data/spec/interceptors/return_response_spec.rb +2 -2
- data/spec/response/data_spec.rb +2 -2
- data/spec/support/zipkin_mock.rb +1 -0
- metadata +27 -21
- data/.rubocop.localch.yml +0 -325
- data/cider-ci.yml +0 -5
- data/cider-ci/bin/bundle +0 -51
- data/cider-ci/bin/ruby_install +0 -8
- data/cider-ci/bin/ruby_version +0 -25
- data/cider-ci/jobs/rspec-activesupport-5.yml +0 -27
- data/cider-ci/jobs/rspec-activesupport-6.yml +0 -28
- data/cider-ci/jobs/rubocop.yml +0 -18
- data/cider-ci/task_components/bundle.yml +0 -22
- data/cider-ci/task_components/rspec.yml +0 -36
- data/cider-ci/task_components/rubocop.yml +0 -29
- data/cider-ci/task_components/ruby.yml +0 -15
@@ -7,6 +7,7 @@ class LHC::Logging < LHC::Interceptor
|
|
7
7
|
|
8
8
|
def before_request
|
9
9
|
return unless logger
|
10
|
+
|
10
11
|
logger.info(
|
11
12
|
[
|
12
13
|
'Before LHC request',
|
@@ -22,6 +23,7 @@ class LHC::Logging < LHC::Interceptor
|
|
22
23
|
|
23
24
|
def after_response
|
24
25
|
return unless logger
|
26
|
+
|
25
27
|
logger.info(
|
26
28
|
[
|
27
29
|
'After LHC response for request',
|
@@ -13,34 +13,64 @@ class LHC::Monitoring < LHC::Interceptor
|
|
13
13
|
|
14
14
|
def before_request
|
15
15
|
return unless statsd
|
16
|
-
|
16
|
+
|
17
|
+
LHC::Monitoring.statsd.count("#{key}.before_request", 1)
|
17
18
|
end
|
18
19
|
|
19
20
|
def after_request
|
20
21
|
return unless statsd
|
21
|
-
|
22
|
-
LHC::Monitoring.statsd.count("#{key
|
22
|
+
|
23
|
+
LHC::Monitoring.statsd.count("#{key}.count", 1)
|
24
|
+
LHC::Monitoring.statsd.count("#{key}.after_request", 1)
|
23
25
|
end
|
24
26
|
|
25
27
|
def after_response
|
26
28
|
return unless statsd
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
|
30
|
+
monitor_time!
|
31
|
+
monitor_cache!
|
32
|
+
monitor_response!
|
31
33
|
end
|
32
34
|
|
33
35
|
private
|
34
36
|
|
35
|
-
def
|
36
|
-
|
37
|
+
def monitor_time!
|
38
|
+
LHC::Monitoring.statsd.timing("#{key}.time", response.time) if response.success?
|
39
|
+
end
|
40
|
+
|
41
|
+
def monitor_cache!
|
42
|
+
return if request.options[:cache].blank?
|
43
|
+
return unless monitor_caching_configuration_check
|
44
|
+
|
45
|
+
if response.from_cache?
|
46
|
+
LHC::Monitoring.statsd.count("#{key}.cache.hit", 1)
|
47
|
+
else
|
48
|
+
LHC::Monitoring.statsd.count("#{key}.cache.miss", 1)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def monitor_caching_configuration_check
|
53
|
+
return true if all_interceptor_classes.include?(LHC::Caching) && all_interceptor_classes.index(self.class) > all_interceptor_classes.index(LHC::Caching)
|
54
|
+
|
55
|
+
warn("[WARNING] Your interceptors must include LHC::Caching and LHC::Monitoring and also in that order.")
|
56
|
+
end
|
57
|
+
|
58
|
+
def monitor_response!
|
59
|
+
if response.timeout?
|
60
|
+
LHC::Monitoring.statsd.count("#{key}.timeout", 1)
|
61
|
+
else
|
62
|
+
LHC::Monitoring.statsd.count("#{key}.#{response.code}", 1)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def key
|
37
67
|
key = options(request.options)[:key]
|
38
68
|
return key if key.present?
|
39
69
|
|
40
70
|
url = sanitize_url(request.url)
|
41
71
|
key = [
|
42
72
|
'lhc',
|
43
|
-
|
73
|
+
module_parent_name.underscore,
|
44
74
|
LHC::Monitoring.env || Rails.env,
|
45
75
|
URI.parse(url).host.gsub(/\./, '_'),
|
46
76
|
request.method
|
@@ -48,8 +78,13 @@ class LHC::Monitoring < LHC::Interceptor
|
|
48
78
|
key.join('.')
|
49
79
|
end
|
50
80
|
|
81
|
+
def module_parent_name
|
82
|
+
(ActiveSupport.gem_version >= Gem::Version.new('6.0.0')) ? Rails.application.class.module_parent_name : Rails.application.class.parent_name
|
83
|
+
end
|
84
|
+
|
51
85
|
def sanitize_url(url)
|
52
|
-
return url if url.match(%r{https?://})
|
86
|
+
return url if url.match?(%r{https?://})
|
87
|
+
|
53
88
|
"http://#{url}"
|
54
89
|
end
|
55
90
|
|
@@ -10,6 +10,7 @@ class LHC::Retry < LHC::Interceptor
|
|
10
10
|
def after_response
|
11
11
|
response.request.options[:retries] ||= 0
|
12
12
|
return unless retry?(response.request)
|
13
|
+
|
13
14
|
response.request.options[:retries] += 1
|
14
15
|
current_retry = response.request.options[:retries]
|
15
16
|
begin
|
@@ -26,6 +27,7 @@ class LHC::Retry < LHC::Interceptor
|
|
26
27
|
return false if request.response.success?
|
27
28
|
return false if request.error_ignored?
|
28
29
|
return false if !request.options.dig(:retry) && !LHC::Retry.all
|
30
|
+
|
29
31
|
request.options[:retries] < max(request)
|
30
32
|
end
|
31
33
|
|
@@ -13,14 +13,17 @@ class LHC::Throttle < LHC::Interceptor
|
|
13
13
|
def before_request
|
14
14
|
options = request.options.dig(:throttle)
|
15
15
|
return unless options
|
16
|
+
|
16
17
|
break_options = options.dig(:break)
|
17
18
|
return unless break_options
|
18
|
-
|
19
|
+
|
20
|
+
break_when_quota_reached! if break_options.match?('%')
|
19
21
|
end
|
20
22
|
|
21
23
|
def after_response
|
22
24
|
options = response.request.options.dig(:throttle)
|
23
25
|
return unless throttle?(options)
|
26
|
+
|
24
27
|
self.class.track ||= {}
|
25
28
|
self.class.track[options.dig(:provider)] = {
|
26
29
|
limit: limit(options: options[:limit], response: response),
|
@@ -40,6 +43,7 @@ class LHC::Throttle < LHC::Interceptor
|
|
40
43
|
track = (self.class.track || {}).dig(options[:provider])
|
41
44
|
return if track.blank? || track[:remaining].blank? || track[:limit].blank? || track[:expires].blank?
|
42
45
|
return if Time.zone.now > track[:expires]
|
46
|
+
|
43
47
|
# avoid floats by multiplying with 100
|
44
48
|
remaining = track[:remaining] * 100
|
45
49
|
limit = track[:limit]
|
@@ -80,7 +84,8 @@ class LHC::Throttle < LHC::Interceptor
|
|
80
84
|
def convert_expires(value)
|
81
85
|
return if value.blank?
|
82
86
|
return value.call(response) if value.is_a?(Proc)
|
83
|
-
return Time.parse(value) if value.match(/GMT/)
|
87
|
+
return Time.parse(value) if value.match?(/GMT/)
|
88
|
+
|
84
89
|
Time.zone.at(value.to_i).to_datetime
|
85
90
|
end
|
86
91
|
end
|
@@ -12,6 +12,7 @@ class LHC::Zipkin < LHC::Interceptor
|
|
12
12
|
|
13
13
|
def before_request
|
14
14
|
return if !dependencies? || !tracing?
|
15
|
+
|
15
16
|
ZipkinTracer::TraceContainer.with_trace_id(trace_id) do
|
16
17
|
# add headers even if the current trace_id should not be sampled
|
17
18
|
B3_HEADERS.each { |method, header| request.headers[header] = trace_id.send(method).to_s }
|
@@ -23,6 +24,7 @@ class LHC::Zipkin < LHC::Interceptor
|
|
23
24
|
def after_response
|
24
25
|
# only sample the current call if we're instructed to do so
|
25
26
|
return unless dependencies? && trace_id.sampled?
|
27
|
+
|
26
28
|
end_trace!
|
27
29
|
end
|
28
30
|
|
data/lib/lhc/request.rb
CHANGED
@@ -25,7 +25,11 @@ class LHC::Request
|
|
25
25
|
interceptors.intercept(:before_raw_request)
|
26
26
|
self.raw = create_request
|
27
27
|
interceptors.intercept(:before_request)
|
28
|
-
|
28
|
+
if self_executing && !response
|
29
|
+
run!
|
30
|
+
elsif response
|
31
|
+
on_complete(response)
|
32
|
+
end
|
29
33
|
end
|
30
34
|
|
31
35
|
def url
|
@@ -63,6 +67,7 @@ class LHC::Request
|
|
63
67
|
|
64
68
|
def optionally_encoded_url(options)
|
65
69
|
return options[:url] unless options.fetch(:url_encoding, true)
|
70
|
+
|
66
71
|
encode_url(options[:url])
|
67
72
|
end
|
68
73
|
|
@@ -81,13 +86,15 @@ class LHC::Request
|
|
81
86
|
|
82
87
|
def translate_body(options)
|
83
88
|
return options if options.fetch(:body, nil).blank?
|
89
|
+
|
84
90
|
options[:body] = format.to_body(options[:body])
|
85
91
|
options
|
86
92
|
end
|
87
93
|
|
88
94
|
def encode_url(url)
|
89
95
|
return url if url.nil?
|
90
|
-
|
96
|
+
|
97
|
+
Addressable::URI.escape(url)
|
91
98
|
end
|
92
99
|
|
93
100
|
def typhoeusize(options)
|
@@ -96,6 +103,7 @@ class LHC::Request
|
|
96
103
|
options.delete(:url)
|
97
104
|
options.each do |key, _v|
|
98
105
|
next if TYPHOEUS_OPTIONS.include? key
|
106
|
+
|
99
107
|
method = "#{key}="
|
100
108
|
options.delete key unless easy.respond_to?(method)
|
101
109
|
end
|
@@ -107,6 +115,7 @@ class LHC::Request
|
|
107
115
|
def use_configured_endpoint!
|
108
116
|
endpoint = LHC.config.endpoints[options[:url]]
|
109
117
|
return unless endpoint
|
118
|
+
|
110
119
|
# explicit options override endpoint options
|
111
120
|
new_options = endpoint.options.deep_merge(options)
|
112
121
|
# set new options
|
@@ -128,13 +137,14 @@ class LHC::Request
|
|
128
137
|
end
|
129
138
|
|
130
139
|
def on_complete(response)
|
131
|
-
self.response = LHC::Response.new(response, self)
|
140
|
+
self.response = response.is_a?(LHC::Response) ? response : LHC::Response.new(response, self)
|
132
141
|
interceptors.intercept(:after_response)
|
133
142
|
handle_error(self.response) unless self.response.success?
|
134
143
|
end
|
135
144
|
|
136
145
|
def handle_error(response)
|
137
146
|
return if ignore_error?
|
147
|
+
|
138
148
|
throw_error(response) unless error_handler
|
139
149
|
response.body_replacement = error_handler.call(response)
|
140
150
|
end
|
data/lib/lhc/response.rb
CHANGED
data/lib/lhc/response/data.rb
CHANGED
data/lib/lhc/version.rb
CHANGED
data/spec/error/to_s_spec.rb
CHANGED
@@ -45,7 +45,7 @@ describe LHC::Error do
|
|
45
45
|
|
46
46
|
context 'some mocked response' do
|
47
47
|
let(:request) do
|
48
|
-
double('
|
48
|
+
double('LHC::Request',
|
49
49
|
method: 'GET',
|
50
50
|
url: 'http://example.com/sessions',
|
51
51
|
headers: { 'Bearer Token' => "aaaaaaaa-bbbb-cccc-dddd-eeee" },
|
@@ -55,7 +55,7 @@ describe LHC::Error do
|
|
55
55
|
end
|
56
56
|
|
57
57
|
let(:response) do
|
58
|
-
double('
|
58
|
+
double('LHC::Response',
|
59
59
|
request: request,
|
60
60
|
code: 500,
|
61
61
|
options: { return_code: :internal_error, response_headers: "" },
|
@@ -64,6 +64,11 @@ describe LHC::Error do
|
|
64
64
|
|
65
65
|
subject { LHC::Error.new('The error message', response) }
|
66
66
|
|
67
|
+
before do
|
68
|
+
allow(request).to receive(:is_a?).with(LHC::Request).and_return(true)
|
69
|
+
allow(response).to receive(:is_a?).with(LHC::Response).and_return(true)
|
70
|
+
end
|
71
|
+
|
67
72
|
it 'produces correct debug output' do
|
68
73
|
expect(subject.to_s.split("\n")).to eq(<<-MSG.strip_heredoc.split("\n"))
|
69
74
|
GET http://example.com/sessions
|
@@ -6,14 +6,14 @@ describe LHC do
|
|
6
6
|
include ActionDispatch::TestProcess
|
7
7
|
|
8
8
|
context 'multipart' do
|
9
|
-
let(:file) {
|
9
|
+
let(:file) { Rack::Test::UploadedFile.new(Tempfile.new) }
|
10
10
|
let(:body) { { size: 2231 }.to_json }
|
11
11
|
let(:location) { 'http://local.ch/uploads/image.jpg' }
|
12
12
|
|
13
13
|
it 'formats requests to be multipart/form-data' do
|
14
14
|
stub_request(:post, 'http://local.ch/') do |request|
|
15
15
|
raise 'Content-Type header wrong' unless request.headers['Content-Type'] == 'multipart/form-data'
|
16
|
-
raise 'Body wrongly formatted' unless request.body.match(/file=%23%3CActionDispatch%3A%3AHttp%3A%3AUploadedFile%3A.*%3E&type=Image/)
|
16
|
+
raise 'Body wrongly formatted' unless request.body.match?(/file=%23%3CActionDispatch%3A%3AHttp%3A%3AUploadedFile%3A.*%3E&type=Image/)
|
17
17
|
end.to_return(status: 200, body: body, headers: { 'Location' => location })
|
18
18
|
response = LHC.multipart.post(
|
19
19
|
'http://local.ch',
|
data/spec/formats/plain_spec.rb
CHANGED
@@ -6,7 +6,7 @@ describe LHC do
|
|
6
6
|
include ActionDispatch::TestProcess
|
7
7
|
|
8
8
|
context 'plain' do
|
9
|
-
let(:file) {
|
9
|
+
let(:file) { Rack::Test::UploadedFile.new(Tempfile.new) }
|
10
10
|
|
11
11
|
it 'leaves plains requests unformatted' do
|
12
12
|
stub_request(:post, 'http://local.ch/')
|
@@ -14,7 +14,7 @@ describe LHC do
|
|
14
14
|
uri = URI.parse(response.request.url)
|
15
15
|
path = [
|
16
16
|
'web',
|
17
|
-
Rails.application.class.parent_name,
|
17
|
+
((ActiveSupport.gem_version >= Gem::Version.new('6.0.0')) ? Rails.application.class.module_parent_name : Rails.application.class.parent_name).underscore,
|
18
18
|
Rails.env,
|
19
19
|
response.request.method,
|
20
20
|
uri.scheme,
|
@@ -47,7 +47,7 @@ describe LHC::Caching do
|
|
47
47
|
|
48
48
|
it 'lets you configure the cache key that will be used' do
|
49
49
|
LHC.config.endpoint(:local, 'http://local.ch', cache: { key: 'STATICKEY' })
|
50
|
-
expect(Rails.cache).to receive(:fetch).with("LHC_CACHE(v#{LHC::Caching::CACHE_VERSION}): STATICKEY").and_call_original
|
50
|
+
expect(Rails.cache).to receive(:fetch).at_least(:once).with("LHC_CACHE(v#{LHC::Caching::CACHE_VERSION}): STATICKEY").and_call_original
|
51
51
|
expect(Rails.cache).to receive(:write).with("LHC_CACHE(v#{LHC::Caching::CACHE_VERSION}): STATICKEY", anything, anything).and_call_original
|
52
52
|
stub
|
53
53
|
LHC.get(:local)
|
@@ -66,8 +66,8 @@ describe LHC::Caching do
|
|
66
66
|
stub
|
67
67
|
LHC.config.endpoint(:local, 'http://local.ch', cache: true)
|
68
68
|
original_response = LHC.get(:local)
|
69
|
-
cached_response = LHC.get(:local)
|
70
69
|
expect(original_response.from_cache?).to eq false
|
70
|
+
cached_response = LHC.get(:local)
|
71
71
|
expect(cached_response.from_cache?).to eq true
|
72
72
|
end
|
73
73
|
end
|
@@ -62,7 +62,8 @@ describe LHC::Caching do
|
|
62
62
|
|
63
63
|
context 'found in central cache' do
|
64
64
|
it 'serves it from central cache if found there' do
|
65
|
-
expect(redis_cache).to receive(:fetch).and_return(nil,
|
65
|
+
expect(redis_cache).to receive(:fetch).and_return(nil,
|
66
|
+
body: '<h1>Hi there</h1>', code: 200, headers: nil, return_code: nil, mock: :webmock)
|
66
67
|
expect(redis_cache).to receive(:write).and_return(true)
|
67
68
|
expect(Rails.cache).to receive(:fetch).and_call_original
|
68
69
|
expect(Rails.cache).to receive(:write).and_call_original
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
describe LHC::Monitoring do
|
6
|
+
let(:stub) do
|
7
|
+
stub_request(:get, 'http://local.ch').to_return(status: 200, body: 'The Website')
|
8
|
+
end
|
9
|
+
|
10
|
+
module Statsd
|
11
|
+
def self.count(_path, _value); end
|
12
|
+
|
13
|
+
def self.timing(_path, _value); end
|
14
|
+
end
|
15
|
+
|
16
|
+
before(:each) do
|
17
|
+
LHC::Monitoring.statsd = Statsd
|
18
|
+
Rails.cache.clear
|
19
|
+
allow(Statsd).to receive(:count).with('lhc.dummy.test.local_ch.get.before_request', 1)
|
20
|
+
allow(Statsd).to receive(:count).with('lhc.dummy.test.local_ch.get.count', 1)
|
21
|
+
allow(Statsd).to receive(:count).with('lhc.dummy.test.local_ch.get.after_request', 1)
|
22
|
+
allow(Statsd).to receive(:count).with('lhc.dummy.test.local_ch.get.200', 1)
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'interceptors configured correctly' do
|
26
|
+
before do
|
27
|
+
LHC.config.interceptors = [LHC::Caching, LHC::Monitoring]
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'requesting with cache option' do
|
31
|
+
it 'monitors miss/hit for caching' do
|
32
|
+
stub
|
33
|
+
expect(Statsd).to receive(:count).with('lhc.dummy.test.local_ch.get.cache.miss', 1)
|
34
|
+
expect(Statsd).to receive(:count).with('lhc.dummy.test.local_ch.get.cache.hit', 1)
|
35
|
+
LHC.get('http://local.ch', cache: true)
|
36
|
+
LHC.get('http://local.ch', cache: true)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'request uncached' do
|
41
|
+
it 'requesting without cache option' do
|
42
|
+
stub
|
43
|
+
expect(Statsd).not_to receive(:count).with('lhc.dummy.test.local_ch.get.cache.miss', 1)
|
44
|
+
expect(Statsd).not_to receive(:count).with('lhc.dummy.test.local_ch.get.cache.hit', 1)
|
45
|
+
LHC.get('http://local.ch')
|
46
|
+
LHC.get('http://local.ch')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'wrong interceptor order' do
|
52
|
+
before(:each) do
|
53
|
+
LHC.config.interceptors = [LHC::Monitoring, LHC::Caching] # monitoring needs to be after Caching
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'does monitors miss/hit for caching and warns about wrong order of interceptors' do
|
57
|
+
stub
|
58
|
+
expect(Statsd).not_to receive(:count).with('lhc.dummy.test.local_ch.get.cache.miss', 1)
|
59
|
+
expect(Statsd).not_to receive(:count).with('lhc.dummy.test.local_ch.get.cache.hit', 1)
|
60
|
+
expect(-> {
|
61
|
+
LHC.get('http://local.ch', cache: true)
|
62
|
+
LHC.get('http://local.ch', cache: true)
|
63
|
+
}).to output("[WARNING] Your interceptors must include LHC::Caching and LHC::Monitoring and also in that order.\n[WARNING] Your interceptors must include LHC::Caching and LHC::Monitoring and also in that order.\n").to_stderr
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|