lhc 13.0.0 → 15.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.
Files changed (63) 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 +89 -0
  7. data/Rakefile +3 -3
  8. data/lhc.gemspec +3 -1
  9. data/lib/lhc.rb +70 -59
  10. data/lib/lhc/concerns/lhc/fix_invalid_encoding_concern.rb +1 -0
  11. data/lib/lhc/config.rb +16 -0
  12. data/lib/lhc/endpoint.rb +3 -0
  13. data/lib/lhc/error.rb +7 -4
  14. data/lib/lhc/interceptor.rb +4 -0
  15. data/lib/lhc/interceptors.rb +1 -0
  16. data/lib/lhc/interceptors/auth.rb +10 -5
  17. data/lib/lhc/interceptors/caching.rb +14 -3
  18. data/lib/lhc/interceptors/logging.rb +4 -2
  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 +3 -2
  22. data/lib/lhc/interceptors/throttle.rb +7 -2
  23. data/lib/lhc/interceptors/zipkin.rb +2 -0
  24. data/lib/lhc/request.rb +37 -4
  25. data/lib/lhc/response.rb +1 -0
  26. data/lib/lhc/response/data.rb +1 -1
  27. data/lib/lhc/scrubber.rb +45 -0
  28. data/lib/lhc/scrubbers/auth_scrubber.rb +33 -0
  29. data/lib/lhc/scrubbers/body_scrubber.rb +28 -0
  30. data/lib/lhc/scrubbers/headers_scrubber.rb +38 -0
  31. data/lib/lhc/scrubbers/params_scrubber.rb +14 -0
  32. data/lib/lhc/version.rb +1 -1
  33. data/spec/config/scrubs_spec.rb +108 -0
  34. data/spec/error/to_s_spec.rb +13 -8
  35. data/spec/formats/multipart_spec.rb +2 -2
  36. data/spec/formats/plain_spec.rb +1 -1
  37. data/spec/interceptors/after_response_spec.rb +1 -1
  38. data/spec/interceptors/caching/main_spec.rb +2 -2
  39. data/spec/interceptors/caching/multilevel_cache_spec.rb +2 -1
  40. data/spec/interceptors/define_spec.rb +1 -0
  41. data/spec/interceptors/logging/main_spec.rb +21 -1
  42. data/spec/interceptors/monitoring/caching_spec.rb +66 -0
  43. data/spec/interceptors/response_competition_spec.rb +2 -2
  44. data/spec/interceptors/return_response_spec.rb +2 -2
  45. data/spec/interceptors/rollbar/main_spec.rb +27 -15
  46. data/spec/request/scrubbed_headers_spec.rb +101 -0
  47. data/spec/request/scrubbed_options_spec.rb +194 -0
  48. data/spec/request/scrubbed_params_spec.rb +35 -0
  49. data/spec/response/data_spec.rb +2 -2
  50. data/spec/support/zipkin_mock.rb +1 -0
  51. metadata +40 -21
  52. data/.rubocop.localch.yml +0 -325
  53. data/cider-ci.yml +0 -5
  54. data/cider-ci/bin/bundle +0 -51
  55. data/cider-ci/bin/ruby_install +0 -8
  56. data/cider-ci/bin/ruby_version +0 -25
  57. data/cider-ci/jobs/rspec-activesupport-5.yml +0 -27
  58. data/cider-ci/jobs/rspec-activesupport-6.yml +0 -28
  59. data/cider-ci/jobs/rubocop.yml +0 -18
  60. data/cider-ci/task_components/bundle.yml +0 -22
  61. data/cider-ci/task_components/rspec.yml +0 -36
  62. data/cider-ci/task_components/rubocop.yml +0 -29
  63. data/cider-ci/task_components/ruby.yml +0 -15
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,17 +65,19 @@ 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
76
80
  debug << _message
77
-
78
81
  debug.map { |str| self.class.fix_invalid_encoding(str) }.join("\n")
79
82
  end
80
83
  end
@@ -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].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].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
@@ -41,15 +41,16 @@ class LHC::Caching < LHC::Interceptor
41
41
 
42
42
  def before_request
43
43
  return unless cache?(request)
44
- key = key(request, options[:key])
45
- response_data = multilevel_cache.fetch(key)
46
- return unless response_data
44
+ return if response_data.blank?
45
+
47
46
  from_cache(request, response_data)
48
47
  end
49
48
 
50
49
  def after_response
51
50
  return unless response.success?
52
51
  return unless cache?(request)
52
+ return if response_data.present?
53
+
53
54
  multilevel_cache.write(
54
55
  key(request, options[:key]),
55
56
  to_cache(response),
@@ -59,6 +60,14 @@ class LHC::Caching < LHC::Interceptor
59
60
 
60
61
  private
61
62
 
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
+
62
71
  # performs read/write (fetch/write) on all configured cache levels (e.g. local & central)
63
72
  def multilevel_cache
64
73
  MultilevelCache.new(
@@ -75,6 +84,7 @@ class LHC::Caching < LHC::Interceptor
75
84
 
76
85
  def central_cache
77
86
  return nil if central.blank? || (central[:read].blank? && central[:write].blank?)
87
+
78
88
  {}.tap do |options|
79
89
  options[:read] = ActiveSupport::Cache::RedisCacheStore.new(url: central[:read]) if central[:read].present?
80
90
  options[:write] = ActiveSupport::Cache::RedisCacheStore.new(url: central[:write]) if central[:write].present?
@@ -86,6 +96,7 @@ class LHC::Caching < LHC::Interceptor
86
96
  # return false if this interceptor cannot work
87
97
  def cache?(request)
88
98
  return false unless request.options[:cache]
99
+
89
100
  (local_cache || central_cache) &&
90
101
  cached_method?(request.method, options[:methods])
91
102
  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
@@ -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
@@ -12,7 +12,13 @@ class LHC::Request
12
12
 
13
13
  TYPHOEUS_OPTIONS ||= [:params, :method, :body, :headers, :follow_location, :params_encoding]
14
14
 
15
- attr_accessor :response, :options, :raw, :format, :error_handler, :errors_ignored, :source
15
+ attr_accessor :response,
16
+ :options,
17
+ :raw,
18
+ :format,
19
+ :error_handler,
20
+ :errors_ignored,
21
+ :source
16
22
 
17
23
  def initialize(options, self_executing = true)
18
24
  self.errors_ignored = (options.fetch(:ignore, []) || []).to_a.compact
@@ -25,7 +31,11 @@ class LHC::Request
25
31
  interceptors.intercept(:before_raw_request)
26
32
  self.raw = create_request
27
33
  interceptors.intercept(:before_request)
28
- run! if self_executing && !response
34
+ if self_executing && !response
35
+ run!
36
+ elsif response
37
+ on_complete(response)
38
+ end
29
39
  end
30
40
 
31
41
  def url
@@ -52,6 +62,23 @@ class LHC::Request
52
62
  raw.run
53
63
  end
54
64
 
65
+ def scrubbed_params
66
+ LHC::ParamsScrubber.new(params.deep_dup).scrubbed
67
+ end
68
+
69
+ def scrubbed_headers
70
+ LHC::HeadersScrubber.new(headers.deep_dup, options[:auth]).scrubbed
71
+ end
72
+
73
+ def scrubbed_options
74
+ scrubbed_options = options.deep_dup
75
+ scrubbed_options[:params] = LHC::ParamsScrubber.new(scrubbed_options[:params]).scrubbed
76
+ scrubbed_options[:headers] = LHC::HeadersScrubber.new(scrubbed_options[:headers], scrubbed_options[:auth]).scrubbed
77
+ scrubbed_options[:auth] = LHC::AuthScrubber.new(scrubbed_options[:auth]).scrubbed
78
+ scrubbed_options[:body] = LHC::BodyScrubber.new(scrubbed_options[:body]).scrubbed
79
+ scrubbed_options
80
+ end
81
+
55
82
  private
56
83
 
57
84
  attr_accessor :interceptors
@@ -63,6 +90,7 @@ class LHC::Request
63
90
 
64
91
  def optionally_encoded_url(options)
65
92
  return options[:url] unless options.fetch(:url_encoding, true)
93
+
66
94
  encode_url(options[:url])
67
95
  end
68
96
 
@@ -81,13 +109,15 @@ class LHC::Request
81
109
 
82
110
  def translate_body(options)
83
111
  return options if options.fetch(:body, nil).blank?
112
+
84
113
  options[:body] = format.to_body(options[:body])
85
114
  options
86
115
  end
87
116
 
88
117
  def encode_url(url)
89
118
  return url if url.nil?
90
- URI.escape(url)
119
+
120
+ Addressable::URI.escape(url)
91
121
  end
92
122
 
93
123
  def typhoeusize(options)
@@ -96,6 +126,7 @@ class LHC::Request
96
126
  options.delete(:url)
97
127
  options.each do |key, _v|
98
128
  next if TYPHOEUS_OPTIONS.include? key
129
+
99
130
  method = "#{key}="
100
131
  options.delete key unless easy.respond_to?(method)
101
132
  end
@@ -107,6 +138,7 @@ class LHC::Request
107
138
  def use_configured_endpoint!
108
139
  endpoint = LHC.config.endpoints[options[:url]]
109
140
  return unless endpoint
141
+
110
142
  # explicit options override endpoint options
111
143
  new_options = endpoint.options.deep_merge(options)
112
144
  # set new options
@@ -128,13 +160,14 @@ class LHC::Request
128
160
  end
129
161
 
130
162
  def on_complete(response)
131
- self.response = LHC::Response.new(response, self)
163
+ self.response = response.is_a?(LHC::Response) ? response : LHC::Response.new(response, self)
132
164
  interceptors.intercept(:after_response)
133
165
  handle_error(self.response) unless self.response.success?
134
166
  end
135
167
 
136
168
  def handle_error(response)
137
169
  return if ignore_error?
170
+
138
171
  throw_error(response) unless error_handler
139
172
  response.body_replacement = error_handler.call(response)
140
173
  end