lhc 13.0.0 → 14.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubocop.yml +15 -0
  3. data/.github/workflows/test.yml +15 -0
  4. data/.rubocop.yml +344 -19
  5. data/.ruby-version +1 -1
  6. data/README.md +44 -0
  7. data/Rakefile +3 -3
  8. data/lhc.gemspec +3 -1
  9. data/lib/lhc.rb +59 -59
  10. data/lib/lhc/concerns/lhc/fix_invalid_encoding_concern.rb +1 -0
  11. data/lib/lhc/config.rb +3 -0
  12. data/lib/lhc/endpoint.rb +3 -0
  13. data/lib/lhc/error.rb +5 -1
  14. data/lib/lhc/interceptor.rb +4 -0
  15. data/lib/lhc/interceptors.rb +1 -0
  16. data/lib/lhc/interceptors/auth.rb +3 -4
  17. data/lib/lhc/interceptors/caching.rb +14 -3
  18. data/lib/lhc/interceptors/logging.rb +2 -0
  19. data/lib/lhc/interceptors/monitoring.rb +46 -11
  20. data/lib/lhc/interceptors/retry.rb +2 -0
  21. data/lib/lhc/interceptors/rollbar.rb +1 -0
  22. data/lib/lhc/interceptors/throttle.rb +7 -2
  23. data/lib/lhc/interceptors/zipkin.rb +2 -0
  24. data/lib/lhc/request.rb +13 -3
  25. data/lib/lhc/response.rb +1 -0
  26. data/lib/lhc/response/data.rb +1 -1
  27. data/lib/lhc/version.rb +1 -1
  28. data/spec/error/to_s_spec.rb +7 -2
  29. data/spec/formats/multipart_spec.rb +2 -2
  30. data/spec/formats/plain_spec.rb +1 -1
  31. data/spec/interceptors/after_response_spec.rb +1 -1
  32. data/spec/interceptors/caching/main_spec.rb +2 -2
  33. data/spec/interceptors/caching/multilevel_cache_spec.rb +2 -1
  34. data/spec/interceptors/define_spec.rb +1 -0
  35. data/spec/interceptors/monitoring/caching_spec.rb +66 -0
  36. data/spec/interceptors/response_competition_spec.rb +2 -2
  37. data/spec/interceptors/return_response_spec.rb +2 -2
  38. data/spec/response/data_spec.rb +2 -2
  39. data/spec/support/zipkin_mock.rb +1 -0
  40. metadata +27 -21
  41. data/.rubocop.localch.yml +0 -325
  42. data/cider-ci.yml +0 -5
  43. data/cider-ci/bin/bundle +0 -51
  44. data/cider-ci/bin/ruby_install +0 -8
  45. data/cider-ci/bin/ruby_version +0 -25
  46. data/cider-ci/jobs/rspec-activesupport-5.yml +0 -27
  47. data/cider-ci/jobs/rspec-activesupport-6.yml +0 -28
  48. data/cider-ci/jobs/rubocop.yml +0 -18
  49. data/cider-ci/task_components/bundle.yml +0 -22
  50. data/cider-ci/task_components/rspec.yml +0 -36
  51. data/cider-ci/task_components/rubocop.yml +0 -29
  52. 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
- LHC::Monitoring.statsd.count("#{key(request)}.before_request", 1)
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
- LHC::Monitoring.statsd.count("#{key(request)}.count", 1)
22
- LHC::Monitoring.statsd.count("#{key(request)}.after_request", 1)
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
- key = key(response)
28
- LHC::Monitoring.statsd.timing("#{key}.time", response.time) if response.success?
29
- key += response.timeout? ? '.timeout' : ".#{response.code}"
30
- LHC::Monitoring.statsd.count(key, 1)
29
+
30
+ monitor_time!
31
+ monitor_cache!
32
+ monitor_response!
31
33
  end
32
34
 
33
35
  private
34
36
 
35
- def key(target)
36
- request = target.is_a?(LHC::Request) ? target : target.request
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
- Rails.application.class.parent_name.underscore,
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
 
@@ -9,6 +9,7 @@ class LHC::Rollbar < LHC::Interceptor
9
9
  def after_response
10
10
  return unless Object.const_defined?('Rollbar')
11
11
  return if response.success?
12
+
12
13
  request = response.request
13
14
  additional_params = request.options.fetch(:rollbar, {})
14
15
  data = {
@@ -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
- break_when_quota_reached! if break_options.match('%')
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
- run! if self_executing && !response
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
- URI.escape(url)
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
@@ -50,6 +50,7 @@ class LHC::Response
50
50
 
51
51
  def format
52
52
  return LHC::Formats::JSON.new if request.nil?
53
+
53
54
  request.format
54
55
  end
55
56
 
@@ -18,7 +18,7 @@ class LHC::Response::Data
18
18
  end
19
19
  end
20
20
 
21
- def method_missing(method, *args, &block) # rubocop:disable Style/MethodMissingSuper
21
+ def method_missing(method, *args, &block)
22
22
  @base.send(method, *args, &block)
23
23
  end
24
24
 
data/lib/lhc/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LHC
4
- VERSION ||= '13.0.0'
4
+ VERSION ||= '14.0.0'
5
5
  end
@@ -45,7 +45,7 @@ describe LHC::Error do
45
45
 
46
46
  context 'some mocked response' do
47
47
  let(:request) do
48
- double('request',
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('response',
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) { fixture_file_upload(Tempfile.new, 'image/jpeg') }
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',
@@ -6,7 +6,7 @@ describe LHC do
6
6
  include ActionDispatch::TestProcess
7
7
 
8
8
  context 'plain' do
9
- let(:file) { fixture_file_upload(Tempfile.new, 'image/jpeg') }
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, body: '<h1>Hi there</h1>', code: 200, headers: nil, return_code: nil, mock: :webmock)
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
@@ -7,6 +7,7 @@ describe LHC do
7
7
  before(:each) do
8
8
  class SomeInterceptor < LHC::Interceptor
9
9
  end
10
+
10
11
  class AnotherInterceptor < LHC::Interceptor
11
12
  end
12
13
  end
@@ -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