bidu-mercy 1.3.0

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