lhc 12.3.0 → 13.4.0.pre.pro1766.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) 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/Gemfile.activesupport5 +1 -1
  7. data/Gemfile.activesupport6 +1 -1
  8. data/README.md +102 -2
  9. data/Rakefile +3 -3
  10. data/lhc.gemspec +6 -3
  11. data/lib/lhc.rb +70 -59
  12. data/lib/lhc/concerns/lhc/fix_invalid_encoding_concern.rb +1 -0
  13. data/lib/lhc/config.rb +16 -0
  14. data/lib/lhc/endpoint.rb +3 -0
  15. data/lib/lhc/error.rb +7 -3
  16. data/lib/lhc/interceptor.rb +4 -0
  17. data/lib/lhc/interceptors.rb +1 -0
  18. data/lib/lhc/interceptors/auth.rb +10 -5
  19. data/lib/lhc/interceptors/caching.rb +70 -44
  20. data/lib/lhc/interceptors/logging.rb +4 -2
  21. data/lib/lhc/interceptors/monitoring.rb +46 -11
  22. data/lib/lhc/interceptors/retry.rb +2 -0
  23. data/lib/lhc/interceptors/rollbar.rb +3 -2
  24. data/lib/lhc/interceptors/throttle.rb +7 -2
  25. data/lib/lhc/interceptors/zipkin.rb +2 -0
  26. data/lib/lhc/railtie.rb +0 -1
  27. data/lib/lhc/request.rb +37 -4
  28. data/lib/lhc/response.rb +1 -0
  29. data/lib/lhc/response/data.rb +1 -1
  30. data/lib/lhc/rspec.rb +1 -2
  31. data/lib/lhc/scrubber.rb +45 -0
  32. data/lib/lhc/scrubbers/auth_scrubber.rb +32 -0
  33. data/lib/lhc/scrubbers/body_scrubber.rb +30 -0
  34. data/lib/lhc/scrubbers/headers_scrubber.rb +40 -0
  35. data/lib/lhc/scrubbers/params_scrubber.rb +14 -0
  36. data/lib/lhc/version.rb +1 -1
  37. data/spec/config/scrubs_spec.rb +108 -0
  38. data/spec/error/to_s_spec.rb +13 -8
  39. data/spec/formats/multipart_spec.rb +2 -2
  40. data/spec/formats/plain_spec.rb +1 -1
  41. data/spec/interceptors/after_response_spec.rb +1 -1
  42. data/spec/interceptors/caching/main_spec.rb +2 -2
  43. data/spec/interceptors/caching/multilevel_cache_spec.rb +139 -0
  44. data/spec/interceptors/caching/options_spec.rb +0 -11
  45. data/spec/interceptors/define_spec.rb +1 -0
  46. data/spec/interceptors/logging/main_spec.rb +21 -1
  47. data/spec/interceptors/monitoring/caching_spec.rb +66 -0
  48. data/spec/interceptors/response_competition_spec.rb +2 -2
  49. data/spec/interceptors/return_response_spec.rb +2 -2
  50. data/spec/interceptors/rollbar/main_spec.rb +27 -15
  51. data/spec/request/scrubbed_headers_spec.rb +101 -0
  52. data/spec/request/scrubbed_options_spec.rb +185 -0
  53. data/spec/request/scrubbed_params_spec.rb +25 -0
  54. data/spec/response/data_spec.rb +2 -2
  55. data/spec/spec_helper.rb +1 -0
  56. data/spec/support/zipkin_mock.rb +1 -0
  57. metadata +59 -26
  58. data/.rubocop.localch.yml +0 -325
  59. data/Gemfile.activesupport4 +0 -4
  60. data/cider-ci.yml +0 -6
  61. data/cider-ci/bin/bundle +0 -51
  62. data/cider-ci/bin/ruby_install +0 -8
  63. data/cider-ci/bin/ruby_version +0 -25
  64. data/cider-ci/jobs/rspec-activesupport-4.yml +0 -28
  65. data/cider-ci/jobs/rspec-activesupport-5.yml +0 -27
  66. data/cider-ci/jobs/rspec-activesupport-6.yml +0 -28
  67. data/cider-ci/jobs/rubocop.yml +0 -18
  68. data/cider-ci/task_components/bundle.yml +0 -22
  69. data/cider-ci/task_components/rspec.yml +0 -36
  70. data/cider-ci/task_components/rubocop.yml +0 -29
  71. data/cider-ci/task_components/ruby.yml +0 -15
data/lib/lhc/endpoint.rb CHANGED
@@ -55,6 +55,7 @@ class LHC::Endpoint
55
55
  # Example: {+datastore}/contracts/{id} == http://local.ch/contracts/1
56
56
  def match?(url)
57
57
  return true if url == uri.pattern
58
+
58
59
  match_data = match_data(url)
59
60
  return false if match_data.nil?
60
61
 
@@ -75,6 +76,7 @@ class LHC::Endpoint
75
76
  def values_as_params(url)
76
77
  match_data = match_data(url)
77
78
  return if match_data.nil?
79
+
78
80
  Hash[match_data.variables.map(&:to_sym).zip(match_data.values)]
79
81
  end
80
82
 
@@ -103,6 +105,7 @@ class LHC::Endpoint
103
105
  # creates params according to template
104
106
  def self.values_as_params(template, url)
105
107
  raise("#{url} does not match the template: #{template}") if !match?(url, template)
108
+
106
109
  new(template).values_as_params(url)
107
110
  end
108
111
 
data/lib/lhc/error.rb CHANGED
@@ -43,6 +43,7 @@ class LHC::Error < StandardError
43
43
 
44
44
  def self.find(response)
45
45
  return LHC::Timeout if response.timeout?
46
+
46
47
  status_code = response.code.to_s[0..2].to_i
47
48
  error = map[status_code]
48
49
  error ||= LHC::UnknownError
@@ -64,12 +65,15 @@ class LHC::Error < StandardError
64
65
  end
65
66
 
66
67
  def to_s
67
- return response if response.is_a?(String)
68
+ return response.to_s unless response.is_a?(LHC::Response)
69
+
68
70
  request = response.request
71
+ return unless request.is_a?(LHC::Request)
72
+
69
73
  debug = []
70
74
  debug << [request.method, request.url].map { |str| self.class.fix_invalid_encoding(str) }.join(' ')
71
- debug << "Options: #{request.options}"
72
- debug << "Headers: #{request.headers}"
75
+ debug << "Options: #{request.scrubbed_options}"
76
+ debug << "Headers: #{request.scrubbed_headers}"
73
77
  debug << "Response Code: #{response.code} (#{response.options[:return_code]})"
74
78
  debug << "Response Options: #{response.options}"
75
79
  debug << response.body
@@ -29,4 +29,8 @@ class LHC::Interceptor
29
29
  def self.dup
30
30
  self
31
31
  end
32
+
33
+ def all_interceptor_classes
34
+ @all_interceptors ||= LHC::Interceptors.new(request).all.map(&:class)
35
+ end
32
36
  end
@@ -19,6 +19,7 @@ class LHC::Interceptors
19
19
  result = interceptor.send(name)
20
20
  if result.is_a? LHC::Response
21
21
  raise 'Response already set from another interceptor' if @response
22
+
22
23
  @response = interceptor.request.response = result
23
24
  end
24
25
  end
@@ -16,6 +16,7 @@ class LHC::Auth < LHC::Interceptor
16
16
  def after_response
17
17
  return unless configuration_correct?
18
18
  return unless reauthenticate?
19
+
19
20
  reauthenticate!
20
21
  end
21
22
 
@@ -29,7 +30,7 @@ class LHC::Auth < LHC::Interceptor
29
30
  def basic_authentication!
30
31
  auth = auth_options[:basic]
31
32
  credentials = "#{auth[:username]}:#{auth[:password]}"
32
- set_authorization_header("Basic #{Base64.strict_encode64(credentials).chomp}")
33
+ set_basic_authorization_header(Base64.strict_encode64(credentials).chomp)
33
34
  end
34
35
 
35
36
  def bearer_authentication!
@@ -43,7 +44,13 @@ class LHC::Auth < LHC::Interceptor
43
44
  request.headers['Authorization'] = value
44
45
  end
45
46
 
47
+ def set_basic_authorization_header(base_64_encoded_credentials)
48
+ request.options[:auth][:basic] = request.options[:auth][:basic].merge(base_64_encoded_credentials: base_64_encoded_credentials)
49
+ set_authorization_header("Basic #{base_64_encoded_credentials}")
50
+ end
51
+
46
52
  def set_bearer_authorization_header(token)
53
+ request.options[:auth] = request.options[:auth].merge(bearer_token: token)
47
54
  set_authorization_header("Bearer #{token}")
48
55
  end
49
56
  # rubocop:enable Style/AccessorMethodName
@@ -75,10 +82,6 @@ class LHC::Auth < LHC::Interceptor
75
82
  @refresh_client_token_option ||= auth_options[:refresh_client_token] || refresh_client_token
76
83
  end
77
84
 
78
- def all_interceptor_classes
79
- @all_interceptors ||= LHC::Interceptors.new(request).all.map(&:class)
80
- end
81
-
82
85
  def auth_options
83
86
  request.options[:auth] || {}
84
87
  end
@@ -90,11 +93,13 @@ class LHC::Auth < LHC::Interceptor
90
93
 
91
94
  def refresh_client_token?
92
95
  return true if refresh_client_token_option.is_a?(Proc)
96
+
93
97
  warn("[WARNING] The given refresh_client_token must be a Proc for reauthentication.")
94
98
  end
95
99
 
96
100
  def retry_interceptor?
97
101
  return true if all_interceptor_classes.include?(LHC::Retry) && all_interceptor_classes.index(LHC::Retry) > all_interceptor_classes.index(self.class)
102
+
98
103
  warn("[WARNING] Your interceptors must include LHC::Retry after LHC::Auth.")
99
104
  end
100
105
  end
@@ -3,69 +3,109 @@
3
3
  class LHC::Caching < LHC::Interceptor
4
4
  include ActiveSupport::Configurable
5
5
 
6
- config_accessor :cache, :logger
6
+ config_accessor :cache, :central
7
7
 
8
+ # to control cache invalidation across all applications in case of
9
+ # breaking changes within this inteceptor
10
+ # that do not lead to cache invalidation otherwise
8
11
  CACHE_VERSION = '1'
9
12
 
10
13
  # Options forwarded to the cache
11
14
  FORWARDED_OPTIONS = [:expires_in, :race_condition_ttl]
12
15
 
16
+ class MultilevelCache
17
+
18
+ def initialize(central: nil, local: nil)
19
+ @central = central
20
+ @local = local
21
+ end
22
+
23
+ def fetch(key)
24
+ central_response = @central[:read].fetch(key) if @central && @central[:read].present?
25
+ if central_response
26
+ puts %Q{[LHC] served from central cache: "#{key}"}
27
+ return central_response
28
+ end
29
+ local_response = @local.fetch(key) if @local
30
+ if local_response
31
+ puts %Q{[LHC] served from local cache: "#{key}"}
32
+ return local_response
33
+ end
34
+ end
35
+
36
+ def write(key, content, options)
37
+ @central[:write].write(key, content, options) if @central && @central[:write].present?
38
+ @local.write(key, content, options) if @local.present?
39
+ end
40
+ end
41
+
13
42
  def before_request
14
43
  return unless cache?(request)
15
- deprecation_warning(request.options)
16
- options = options(request.options)
17
- key = key(request, options[:key])
18
- response_data = cache_for(options).fetch(key)
19
- return unless response_data
20
- logger&.info "Served from cache: #{key}"
44
+ return if response_data.blank?
45
+
21
46
  from_cache(request, response_data)
22
47
  end
23
48
 
24
49
  def after_response
25
50
  return unless response.success?
26
- request = response.request
27
51
  return unless cache?(request)
28
- options = options(request.options)
29
- cache_for(options).write(
52
+ return if response_data.present?
53
+
54
+ multilevel_cache.write(
30
55
  key(request, options[:key]),
31
56
  to_cache(response),
32
- cache_options(options)
57
+ cache_options
33
58
  )
34
59
  end
35
60
 
36
61
  private
37
62
 
38
- # return the cache for the given options
39
- def cache_for(options)
63
+ # from cache
64
+ def response_data
65
+ # stop calling multi-level cache if it already returned nil for this interceptor instance
66
+ return @response_data if defined? @response_data
67
+
68
+ @response_data ||= multilevel_cache.fetch(key(request, options[:key]))
69
+ end
70
+
71
+ # performs read/write (fetch/write) on all configured cache levels (e.g. local & central)
72
+ def multilevel_cache
73
+ MultilevelCache.new(
74
+ central: central_cache,
75
+ local: local_cache
76
+ )
77
+ end
78
+
79
+ # returns the local cache either configured for entire LHC
80
+ # or configured locally for that particular request
81
+ def local_cache
40
82
  options.fetch(:use, cache)
41
83
  end
42
84
 
85
+ def central_cache
86
+ return nil if central.blank? || (central[:read].blank? && central[:write].blank?)
87
+
88
+ {}.tap do |options|
89
+ options[:read] = ActiveSupport::Cache::RedisCacheStore.new(url: central[:read]) if central[:read].present?
90
+ options[:write] = ActiveSupport::Cache::RedisCacheStore.new(url: central[:write]) if central[:write].present?
91
+ end
92
+ end
93
+
43
94
  # do we even need to bother with this interceptor?
44
95
  # based on the options, this method will
45
96
  # return false if this interceptor cannot work
46
97
  def cache?(request)
47
98
  return false unless request.options[:cache]
48
- options = options(request.options)
49
- cache_for(options) &&
99
+
100
+ (local_cache || central_cache) &&
50
101
  cached_method?(request.method, options[:methods])
51
102
  end
52
103
 
53
- # returns the request_options
54
- # will map deprecated options to the new format
55
- def options(request_options)
56
- options = (request_options[:cache] == true) ? {} : request_options[:cache].dup
57
- map_deprecated_options!(request_options, options)
104
+ def options
105
+ options = (request.options[:cache] == true) ? {} : request.options[:cache].dup
58
106
  options
59
107
  end
60
108
 
61
- # maps `cache_key` -> `key`, `cache_expires_in` -> `expires_in` and so on
62
- def map_deprecated_options!(request_options, options)
63
- deprecated_keys(request_options).each do |deprecated_key|
64
- new_key = deprecated_key.to_s.gsub(/^cache_/, '').to_sym
65
- options[new_key] = request_options[deprecated_key]
66
- end
67
- end
68
-
69
109
  # converts json we read from the cache to an LHC::Response object
70
110
  def from_cache(request, data)
71
111
  raw = Typhoeus::Response.new(data)
@@ -104,24 +144,10 @@ class LHC::Caching < LHC::Interceptor
104
144
 
105
145
  # extracts the options that should be forwarded to
106
146
  # the cache
107
- def cache_options(input = {})
108
- input.each_with_object({}) do |(key, value), result|
147
+ def cache_options
148
+ options.each_with_object({}) do |(key, value), result|
109
149
  result[key] = value if key.in? FORWARDED_OPTIONS
110
150
  result
111
151
  end
112
152
  end
113
-
114
- # grabs the deprecated keys from the request options
115
- def deprecated_keys(request_options)
116
- request_options.keys.select { |k| k =~ /^cache_.*/ }.sort
117
- end
118
-
119
- # emits a deprecation warning if necessary
120
- def deprecation_warning(request_options)
121
- unless deprecated_keys(request_options).empty?
122
- ActiveSupport::Deprecation.warn(
123
- "Cache options have changed! #{deprecated_keys(request_options).join(', ')} are deprecated and will be removed in future versions."
124
- )
125
- end
126
- end
127
153
  end
@@ -7,14 +7,15 @@ 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',
13
14
  "<#{request.object_id}>",
14
15
  request.method.upcase,
15
16
  "#{request.url} at #{Time.now.iso8601}",
16
- "Params=#{request.params}",
17
- "Headers=#{request.headers}",
17
+ "Params=#{request.scrubbed_params}",
18
+ "Headers=#{request.scrubbed_headers}",
18
19
  request.source ? "\nCalled from #{request.source}" : nil
19
20
  ].compact.join(' ')
20
21
  )
@@ -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 = {
@@ -22,8 +23,8 @@ class LHC::Rollbar < LHC::Interceptor
22
23
  request: {
23
24
  url: request.url,
24
25
  method: request.method,
25
- headers: request.headers,
26
- params: request.params
26
+ headers: request.scrubbed_headers,
27
+ params: request.scrubbed_params
27
28
  }
28
29
  }.merge additional_params
29
30
  begin
@@ -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