mercy 1.3.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +88 -0
  6. data/LICENSE +22 -0
  7. data/README.md +100 -0
  8. data/Rakefile +7 -0
  9. data/circle.yml +16 -0
  10. data/lib/bidu.rb +3 -0
  11. data/lib/bidu/mercy.rb +18 -0
  12. data/lib/bidu/mercy/class_methods.rb +19 -0
  13. data/lib/bidu/mercy/concern.rb +10 -0
  14. data/lib/bidu/mercy/report.rb +44 -0
  15. data/lib/bidu/mercy/report/active_record.rb +28 -0
  16. data/lib/bidu/mercy/report/error.rb +62 -0
  17. data/lib/bidu/mercy/report/multiple.rb +29 -0
  18. data/lib/bidu/mercy/report/range.rb +54 -0
  19. data/lib/bidu/mercy/report_builder.rb +24 -0
  20. data/lib/bidu/mercy/report_config.rb +37 -0
  21. data/lib/bidu/mercy/status.rb +27 -0
  22. data/lib/bidu/mercy/status_builder.rb +35 -0
  23. data/lib/bidu/mercy/version.rb +5 -0
  24. data/lib/bidu/period_parser.rb +32 -0
  25. data/lib/json_parser/type_cast_ext.rb +7 -0
  26. data/mercy.gemspec +33 -0
  27. data/spec/lib/bidu/mercy/report/error_spec.rb +385 -0
  28. data/spec/lib/bidu/mercy/report/multiple_spec.rb +122 -0
  29. data/spec/lib/bidu/mercy/report/range_spec.rb +302 -0
  30. data/spec/lib/bidu/mercy/report/report_config_spec.rb +39 -0
  31. data/spec/lib/bidu/mercy/report_builder_spec.rb +72 -0
  32. data/spec/lib/bidu/mercy/report_spec.rb +44 -0
  33. data/spec/lib/bidu/mercy/status_builder_spec.rb +84 -0
  34. data/spec/lib/bidu/mercy/status_spec.rb +135 -0
  35. data/spec/lib/bidu/period_parser_spec.rb +27 -0
  36. data/spec/spec_helper.rb +32 -0
  37. data/spec/support/fixture_helpers.rb +19 -0
  38. data/spec/support/models/document.rb +6 -0
  39. data/spec/support/report/dummy.rb +17 -0
  40. data/spec/support/schema.rb +11 -0
  41. metadata +236 -0
@@ -0,0 +1,29 @@
1
+ module Bidu::Mercy::Report::Multiple
2
+ def as_json
3
+ {
4
+ status: status
5
+ }.merge(sub_reports_hash)
6
+ end
7
+
8
+ def error?
9
+ sub_reports.any?(&:error?)
10
+ end
11
+
12
+ private
13
+
14
+ def sub_reports_hash
15
+ sub_reports.map(&:as_json).as_hash(reports_ids.map(&:to_s))
16
+ end
17
+
18
+ def sub_reports
19
+ @sub_reports ||= reports_ids.map do |id|
20
+ build_sub_report(id)
21
+ end
22
+ end
23
+
24
+ def build_sub_report(id)
25
+ sub_report_class.new(json.merge(key => id))
26
+ end
27
+ end
28
+
29
+
@@ -0,0 +1,54 @@
1
+ module Bidu
2
+ module Mercy
3
+ class Report
4
+ class Range < Report::ActiveRecord
5
+ ALLOWED_PARAMETERS=[:period, :maximum, :minimum]
6
+ DEFAULT_OPTION = {
7
+ period: 1.day,
8
+ scope: :all,
9
+ minimum: nil,
10
+ maximum: nil
11
+ }
12
+
13
+ json_parse :scope
14
+ json_parse :minimum, :maximum, type: :integer
15
+
16
+ def initialize(options)
17
+ super(DEFAULT_OPTION.merge(options))
18
+ end
19
+
20
+ def scoped
21
+ @scoped ||= fetch_scoped(last_entries, scope)
22
+ end
23
+
24
+ def error?
25
+ @error ||= !count_in_range?
26
+ end
27
+
28
+ def as_json
29
+ {
30
+ status: status,
31
+ count: count
32
+ }
33
+ end
34
+
35
+ def count
36
+ scoped.count
37
+ end
38
+
39
+ private
40
+
41
+ def range
42
+ (minimum..maximum)
43
+ end
44
+
45
+ def count_in_range?
46
+ return range.include?(count) unless (maximum.nil? || minimum.nil?)
47
+ return count >= minimum unless minimum.nil?
48
+ return count <= maximum unless maximum.nil?
49
+ true
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,24 @@
1
+ module Bidu
2
+ module Mercy
3
+ class ReportBuilder
4
+ def build(key, parameters = {})
5
+ config = config_for(key)
6
+ config.build(parameters)
7
+ end
8
+
9
+ def add_config(key, config)
10
+ configs[key] = Bidu::Mercy::ReportConfig.new(config)
11
+ end
12
+
13
+ private
14
+
15
+ def config_for(key)
16
+ configs[key]
17
+ end
18
+
19
+ def configs
20
+ @configs ||= {}
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ module Bidu
2
+ module Mercy
3
+ class ReportConfig
4
+ attr_accessor :config
5
+
6
+ delegate :[], :[]=, :merge, to: :config
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def build(parameters)
13
+ params = slice_parameters(parameters)
14
+ report_class.new(config.merge(params))
15
+ end
16
+
17
+ private
18
+
19
+ def type
20
+ self[:type] ||= :error
21
+ end
22
+
23
+ def report_class
24
+ return type if type.is_a?(Class)
25
+ @report_class ||= Bidu::Mercy::Report.const_get(type.to_s.camelize)
26
+ end
27
+
28
+ def slice_parameters(parameters)
29
+ parameters.slice(*allowed_parameters)
30
+ end
31
+
32
+ def allowed_parameters
33
+ report_class::ALLOWED_PARAMETERS
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ module Bidu
2
+ module Mercy
3
+ class Status
4
+ attr_reader :reports
5
+
6
+ def initialize(reports)
7
+ @reports = reports
8
+ end
9
+
10
+ def status
11
+ reports.any? { |r| r.error? } ? :error : :ok
12
+ end
13
+
14
+ def as_json
15
+ {
16
+ status: status
17
+ }.merge(reports_jsons)
18
+ end
19
+
20
+ private
21
+
22
+ def reports_jsons
23
+ reports.map(&:as_json).as_hash(reports.map(&:id))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ module Bidu
2
+ module Mercy
3
+ class StatusBuilder
4
+ def build(key, parameters = {})
5
+ Bidu::Mercy::Status.new(reports_for(key, parameters))
6
+ end
7
+
8
+ def add_report_config(key, config)
9
+ status_key = config.delete(:on) || :default
10
+ report_builder.add_config(key, config)
11
+ config_for(status_key) << key
12
+ end
13
+
14
+ private
15
+
16
+ def report_builder
17
+ @report_builder ||= Bidu::Mercy::ReportBuilder.new
18
+ end
19
+
20
+ def reports_for(key, parameters)
21
+ config_for(key).map do |report_key|
22
+ report_builder.build(report_key, parameters)
23
+ end
24
+ end
25
+
26
+ def configs
27
+ @configs ||= {}
28
+ end
29
+
30
+ def config_for(key)
31
+ configs[key] ||= []
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ module Bidu
2
+ module Mercy
3
+ VERSION = '1.3.0'
4
+ end
5
+ end
@@ -0,0 +1,32 @@
1
+ module Bidu
2
+ class PeriodParser
3
+ class << self
4
+ def parse(period)
5
+ return unless period
6
+ return period if period.is_a?(Integer)
7
+ new(period).to_seconds
8
+ end
9
+ end
10
+
11
+ def initialize(period)
12
+ @period = period
13
+ end
14
+
15
+ def to_seconds
16
+ return unless period.match(/^\d+(years|months|days|hours|minutes|seconds)?/)
17
+ type.blank? ? value.seconds : value.public_send(type)
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :period
23
+
24
+ def value
25
+ @period_value ||= period.gsub(/\D+/, '').to_i
26
+ end
27
+
28
+ def type
29
+ @period_type ||= period.gsub(/\d+/, '')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ module JsonParser
2
+ module TypeCast
3
+ def to_period(value)
4
+ Bidu::PeriodParser.parse(value)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bidu/mercy/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'mercy'
8
+ gem.version = Bidu::Mercy::VERSION
9
+ gem.authors = ["Bidu Dev's Team"]
10
+ gem.email = ["dev@bidu.com.br"]
11
+ gem.homepage = 'https://github.com/Bidu/mercy'
12
+ gem.description = 'Gem for easy health check'
13
+ gem.summary = gem.description
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|gem|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency 'activesupport'
21
+ gem.add_runtime_dependency 'concern_builder'
22
+ gem.add_runtime_dependency 'darthjee-active_ext'
23
+ gem.add_runtime_dependency 'json_parser', '~> 1.1'
24
+
25
+ gem.add_development_dependency "activerecord"
26
+ gem.add_development_dependency "sqlite3"
27
+
28
+ gem.add_development_dependency "bundler"
29
+ gem.add_development_dependency "rake"
30
+ gem.add_development_dependency "rspec"
31
+ gem.add_development_dependency 'pry-nav'
32
+ gem.add_development_dependency 'simplecov'
33
+ end
@@ -0,0 +1,385 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bidu::Mercy::Report::Error do
4
+ let(:errors) { 1 }
5
+ let(:successes) { 1 }
6
+ let(:old_errors) { 2 }
7
+ let(:threshold) { 0.02 }
8
+ let(:period) { 1.day }
9
+ let(:external_key) { :external_id }
10
+ let(:scope) { :with_error }
11
+ let(:base_scope) { :all }
12
+ let(:options) do
13
+ {
14
+ period: period,
15
+ threshold: threshold,
16
+ scope: scope,
17
+ base_scope: base_scope,
18
+ clazz: Document,
19
+ external_key: external_key
20
+ }
21
+ end
22
+ let(:subject) { described_class.new(options) }
23
+ let(:types) { [:a] }
24
+ before do
25
+ Document.all.each(&:destroy)
26
+ types.each do |type|
27
+ successes.times { Document.create status: :success, doc_type: type }
28
+ errors.times do |i|
29
+ Document.create status: :error, external_id: 10 * successes + i, outter_external_id: i, doc_type: type
30
+ end
31
+ old_errors.times do
32
+ Document.create status: :error, created_at: 2.days.ago, updated_at: 2.days.ago, doc_type: type
33
+ end
34
+ end
35
+ end
36
+
37
+ describe '#status' do
38
+ context 'when are more errors than the allowed by the threshold' do
39
+ let(:errors) { 1 }
40
+ let(:successes) { 3 }
41
+ it { expect(subject.status).to eq(:error) }
42
+ end
43
+
44
+ context 'when the threshold is 0 and there are no errors' do
45
+ let(:errors) { 0 }
46
+ let(:threshold) { 0 }
47
+ it { expect(subject.status).to eq(:ok) }
48
+ end
49
+
50
+ context 'when the threshold is 100% and there is 100% error' do
51
+ let(:successes) { 0 }
52
+ let(:threshold) { 1 }
53
+ it { expect(subject.status).to eq(:ok) }
54
+ end
55
+
56
+ context 'when there are no documents' do
57
+ let(:successes) { 0 }
58
+ let(:errors) { 0 }
59
+ it { expect(subject.status).to eq(:ok) }
60
+ end
61
+
62
+ context 'when there are older errors out of the period' do
63
+ let(:threshold) { 0.6 }
64
+
65
+ it 'ignores the older errros' do
66
+ expect(subject.status).to eq(:ok)
67
+ end
68
+
69
+ context 'when passing a bigger period' do
70
+ let(:period) { 3.days }
71
+
72
+ it 'consider the older errros' do
73
+ expect(subject.status).to eq(:error)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ describe 'percentage' do
80
+ context 'when there are 25% erros' do
81
+ let(:errors) { 1 }
82
+ let(:successes) { 3 }
83
+ it { expect(subject.percentage).to eq(0.25) }
84
+ end
85
+
86
+ context 'when there are no errors' do
87
+ let(:errors) { 0 }
88
+ let(:threshold) { 0 }
89
+ it { expect(subject.percentage).to eq(0) }
90
+ end
91
+
92
+ context 'when there is 100% error' do
93
+ let(:successes) { 0 }
94
+ let(:threshold) { 1 }
95
+ it { expect(subject.percentage).to eq(1) }
96
+ end
97
+
98
+ context 'when there are no documents' do
99
+ let(:successes) { 0 }
100
+ let(:errors) { 0 }
101
+ it { expect(subject.percentage).to eq(0) }
102
+ end
103
+
104
+ context 'when there are older errors out of the period' do
105
+ let(:old_errors) { 2 }
106
+
107
+ it 'ignores the older errros' do
108
+ expect(subject.percentage).to eq(0.5)
109
+ end
110
+
111
+ context 'when passing a bigger period' do
112
+ let(:period) { 3.days }
113
+
114
+ it 'consider the older errros' do
115
+ expect(subject.percentage).to eq(0.75)
116
+ end
117
+ end
118
+ end
119
+
120
+ context 'when configuring with a complex scope' do
121
+ let(:types) { [:a, :b] }
122
+ let(:old_errors) { 0 }
123
+ let(:scope) { :'with_error.type_b' }
124
+ let(:errors) { 1 }
125
+ let(:successes) { 3 }
126
+ context 'as symbol' do
127
+ let(:scope) { :'with_error.type_b' }
128
+
129
+ it 'fetches from each scope in order' do
130
+ expect(subject.percentage).to eq(0.125)
131
+ end
132
+ end
133
+
134
+ context 'as string where scope' do
135
+ let(:scope) { "status = 'error' and doc_type = 'b'" }
136
+
137
+ it 'fetches from each scope in order' do
138
+ expect(subject.percentage).to eq(0.125)
139
+ end
140
+ end
141
+
142
+ context 'as hash where scope' do
143
+ let(:scope) { { status: :error, doc_type: :b } }
144
+
145
+ it 'fetches from each scope in order' do
146
+ expect(subject.percentage).to eq(0.125)
147
+ end
148
+ end
149
+ end
150
+
151
+ context 'when using a base scope' do
152
+ let(:types) { [:a, :b, :b, :b] }
153
+ let(:old_errors) { 0 }
154
+ let(:errors) { 1 }
155
+ let(:successes) { 3 }
156
+
157
+ context 'as symbol' do
158
+ let(:base_scope) { :type_b }
159
+
160
+ it 'fetches from each scope in order' do
161
+ expect(subject.percentage).to eq(0.25)
162
+ end
163
+ end
164
+
165
+ context 'as where clause' do
166
+ let(:base_scope) { "doc_type = 'b'" }
167
+
168
+ it 'fetches from each scope in order' do
169
+ expect(subject.percentage).to eq(0.25)
170
+ end
171
+ end
172
+
173
+ context 'as hash' do
174
+ let(:base_scope) { { doc_type: :b } }
175
+
176
+ it 'fetches from each scope in order' do
177
+ expect(subject.percentage).to eq(0.25)
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ describe '#scoped' do
184
+ context 'when there are 25% erros' do
185
+ let(:errors) { 1 }
186
+ let(:successes) { 3 }
187
+ it 'returns only the scoped documents' do
188
+ expect(subject.scoped.count).to eq(1)
189
+ end
190
+ end
191
+
192
+ context 'when there are no errors' do
193
+ let(:errors) { 0 }
194
+ let(:threshold) { 0 }
195
+ it { expect(subject.scoped).to be_empty }
196
+ end
197
+
198
+ context 'when there are no documents' do
199
+ let(:successes) { 0 }
200
+ let(:errors) { 0 }
201
+ it { expect(subject.scoped).to be_empty }
202
+ end
203
+
204
+ context 'when there are older errors out of the period' do
205
+ let(:old_errors) { 2 }
206
+
207
+ it 'ignores the older errros' do
208
+ expect(subject.scoped.count).to eq(1)
209
+ end
210
+
211
+ context 'when passing a bigger period' do
212
+ let(:period) { 3.days }
213
+
214
+ it 'consider the older errros' do
215
+ expect(subject.scoped.count).to eq(3)
216
+ end
217
+ end
218
+ end
219
+
220
+ context 'when configured with a complex scope' do
221
+ let(:types) { [:a, :b, :b] }
222
+ let(:old_errors) { 0 }
223
+
224
+ context 'as symbol' do
225
+ let(:scope) { :'with_error.type_b' }
226
+
227
+ it 'fetches from each scope in order' do
228
+ expect(subject.scoped.count).to eq(Document.with_error.type_b.count)
229
+ expect(subject.scoped.count).to eq(2 * Document.with_error.type_a.count)
230
+ end
231
+ end
232
+
233
+ context 'as hash' do
234
+ let(:scope) { { status: :error, doc_type: :b } }
235
+
236
+ it 'fetches from each scope in order' do
237
+ expect(subject.scoped.count).to eq(Document.with_error.type_b.count)
238
+ expect(subject.scoped.count).to eq(2 * Document.with_error.type_a.count)
239
+ end
240
+ end
241
+
242
+ context 'as string where scope' do
243
+ let(:scope) { "status = 'error' and doc_type = 'b'" }
244
+
245
+ it 'fetches from each scope in order' do
246
+ expect(subject.scoped.count).to eq(Document.with_error.type_b.count)
247
+ expect(subject.scoped.count).to eq(2 * Document.with_error.type_a.count)
248
+ end
249
+ end
250
+ end
251
+
252
+ context 'when using a base scope' do
253
+ let(:types) { [:a, :b, :b, :b] }
254
+ let(:old_errors) { 0 }
255
+
256
+ context 'as symbol' do
257
+ let(:base_scope) { :type_b }
258
+
259
+ it 'fetches from each scope in order' do
260
+ expect(subject.scoped.count).to eq(Document.with_error.type_b.count)
261
+ expect(subject.scoped.count).to eq(3 * Document.with_error.type_a.count)
262
+ end
263
+ end
264
+
265
+ context 'as where clause' do
266
+ let(:base_scope) { "doc_type = 'b'" }
267
+
268
+ it 'fetches from each scope in order' do
269
+ expect(subject.scoped.count).to eq(Document.with_error.type_b.count)
270
+ expect(subject.scoped.count).to eq(3 * Document.with_error.type_a.count)
271
+ end
272
+ end
273
+
274
+ context 'as hash' do
275
+ let(:base_scope) { { doc_type: :b } }
276
+
277
+ it 'fetches from each scope in order' do
278
+ expect(subject.scoped.count).to eq(Document.with_error.type_b.count)
279
+ expect(subject.scoped.count).to eq(3 * Document.with_error.type_a.count)
280
+ end
281
+ end
282
+ end
283
+ end
284
+
285
+ describe '#error?' do
286
+ context 'when errors percentage overcames threshold' do
287
+ it { expect(subject.error?).to be_truthy }
288
+ end
289
+
290
+ context 'when errors percentage does not overcames threshold' do
291
+ let(:errors) { 0 }
292
+ it { expect(subject.error?).to be_falsey }
293
+ end
294
+ end
295
+
296
+ describe '#status' do
297
+ context 'when errors percentage overcames threshold' do
298
+ it { expect(subject.status).to eq(:error) }
299
+ end
300
+
301
+ context 'when errors percentage does not overcames threshold' do
302
+ let(:errors) { 0 }
303
+ it { expect(subject.status).to eq(:ok) }
304
+ end
305
+ end
306
+
307
+ describe '#as_json' do
308
+ let(:expected) do
309
+ { ids: ids_expected, percentage: percentage_expected, status: status_expected }
310
+ end
311
+
312
+ context 'when everything is ok' do
313
+ let(:errors) { 1 }
314
+ let(:successes) { 9 }
315
+ let(:ids_expected) { [90] }
316
+ let(:status_expected) { :ok }
317
+ let(:percentage_expected) { 0.1 }
318
+ let(:threshold) { 0.5 }
319
+
320
+ it 'returns the external keys, status and error percentage' do
321
+ expect(subject.as_json).to eq(expected)
322
+ end
323
+
324
+ end
325
+
326
+ context 'when there are 75% erros' do
327
+ let(:status_expected) { :error }
328
+ let(:percentage_expected) { 0.75 }
329
+ let(:errors) { 3 }
330
+ let(:successes) { 1 }
331
+ let(:ids_expected) { [10, 11, 12] }
332
+
333
+ it 'returns the external keys, status and error percentage' do
334
+ expect(subject.as_json).to eq(expected)
335
+ end
336
+
337
+ context 'when configurated with different external key' do
338
+ let(:external_key) { :outter_external_id }
339
+ let(:ids_expected) { [0, 1, 2] }
340
+
341
+ it 'returns the correct external keys' do
342
+ expect(subject.as_json).to eq(expected)
343
+ end
344
+
345
+ context 'when some external ids are the same' do
346
+ let(:ids_expected) { [10, 10, 10] }
347
+ before do
348
+ Document.update_all(outter_external_id: 10)
349
+ end
350
+
351
+ it 'returns the correct external keys' do
352
+ expect(subject.as_json).to eq(expected)
353
+ end
354
+
355
+ context 'and passing uniq option' do
356
+ before { options[:uniq] = true }
357
+ let(:ids_expected) { [10] }
358
+
359
+ it 'returns the correct external keys only once' do
360
+ expect(subject.as_json).to eq(expected)
361
+ end
362
+ end
363
+ end
364
+
365
+ context 'with a limit' do
366
+ before { options[:limit] = 2 }
367
+ let(:ids_expected) { [0, 1] }
368
+
369
+ it 'returns only the limited ids' do
370
+ expect(subject.as_json).to eq(expected)
371
+ end
372
+ end
373
+ end
374
+
375
+ context 'when configurated without external key' do
376
+ before { options.delete(:external_key) }
377
+ let(:ids_expected) { Document.with_error.where('created_at > ?', 30.hours.ago).map(&:id) }
378
+
379
+ it 'returns the ids as default id' do
380
+ expect(subject.as_json).to eq(expected)
381
+ end
382
+ end
383
+ end
384
+ end
385
+ end