lhc 13.0.0 → 15.0.0

Sign up to get free protection for your applications and to get access to all the features.
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