logster 1.3.0 → 1.3.1

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +18 -18
  3. data/.travis.yml +15 -15
  4. data/CHANGELOG.md +137 -130
  5. data/Gemfile +4 -4
  6. data/Guardfile +8 -8
  7. data/LICENSE.txt +22 -22
  8. data/README.md +99 -99
  9. data/Rakefile +24 -24
  10. data/assets/fonts/FontAwesome.otf +0 -0
  11. data/assets/fonts/fontawesome-webfont.eot +0 -0
  12. data/assets/fonts/fontawesome-webfont.svg +639 -639
  13. data/assets/fonts/fontawesome-webfont.ttf +0 -0
  14. data/assets/fonts/fontawesome-webfont.woff +0 -0
  15. data/assets/fonts/fontawesome-webfont.woff2 +0 -0
  16. data/assets/images/Icon-144_rounded.png +0 -0
  17. data/assets/images/Icon-144_square.png +0 -0
  18. data/assets/images/icon_144x144.png +0 -0
  19. data/assets/images/icon_64x64.png +0 -0
  20. data/assets/javascript/client-app.js +81 -81
  21. data/assets/javascript/vendor.js +5302 -5302
  22. data/assets/stylesheets/client-app.css +0 -0
  23. data/assets/stylesheets/vendor.css +3 -3
  24. data/build_client_app.sh +12 -12
  25. data/client-app/.editorconfig +20 -20
  26. data/client-app/.ember-cli +9 -9
  27. data/client-app/.eslintignore +19 -19
  28. data/client-app/.eslintrc.js +46 -46
  29. data/client-app/.gitignore +23 -23
  30. data/client-app/.travis.yml +27 -27
  31. data/client-app/.watchmanconfig +3 -3
  32. data/client-app/README.md +57 -57
  33. data/client-app/app/app.js +14 -14
  34. data/client-app/app/components/message-info.js +18 -18
  35. data/client-app/app/components/message-row.js +45 -45
  36. data/client-app/app/components/panel-resizer.js +75 -75
  37. data/client-app/app/components/tab-contents.js +27 -27
  38. data/client-app/app/components/tab-link.js +5 -5
  39. data/client-app/app/components/tabbed-section.js +32 -32
  40. data/client-app/app/components/time-formatter.js +25 -25
  41. data/client-app/app/components/update-time.js +21 -21
  42. data/client-app/app/controllers/index.js +83 -83
  43. data/client-app/app/controllers/show.js +13 -13
  44. data/client-app/app/index.html +29 -29
  45. data/client-app/app/initializers/app-init.js +55 -55
  46. data/client-app/app/lib/preload.js +14 -14
  47. data/client-app/app/lib/utilities.js +140 -140
  48. data/client-app/app/models/message-collection.js +158 -158
  49. data/client-app/app/models/message.js +99 -99
  50. data/client-app/app/resolver.js +3 -3
  51. data/client-app/app/router.js +14 -14
  52. data/client-app/app/routes/index.js +53 -53
  53. data/client-app/app/routes/show.js +14 -14
  54. data/client-app/app/styles/app.css +387 -387
  55. data/client-app/app/templates/application.hbs +2 -2
  56. data/client-app/app/templates/components/message-info.hbs +44 -44
  57. data/client-app/app/templates/components/message-row.hbs +17 -17
  58. data/client-app/app/templates/components/tabbed-section.hbs +10 -10
  59. data/client-app/app/templates/components/time-formatter.hbs +1 -1
  60. data/client-app/app/templates/index.hbs +57 -57
  61. data/client-app/app/templates/show.hbs +4 -4
  62. data/client-app/config/environment.js +51 -51
  63. data/client-app/config/optional-features.json +3 -3
  64. data/client-app/config/targets.js +18 -18
  65. data/client-app/ember-cli-build.js +29 -29
  66. data/client-app/package-lock.json +11365 -11365
  67. data/client-app/package.json +56 -56
  68. data/client-app/testem.js +25 -25
  69. data/client-app/tests/index.html +34 -34
  70. data/client-app/tests/integration/components/message-info-test.js +26 -26
  71. data/client-app/tests/integration/components/message-row-test.js +26 -26
  72. data/client-app/tests/integration/components/panel-resizer-test.js +26 -26
  73. data/client-app/tests/integration/components/tab-contents-test.js +26 -26
  74. data/client-app/tests/integration/components/tab-link-test.js +26 -26
  75. data/client-app/tests/integration/components/tabbed-section-test.js +26 -26
  76. data/client-app/tests/integration/components/time-formatter-test.js +26 -26
  77. data/client-app/tests/integration/components/update-time-test.js +26 -26
  78. data/client-app/tests/test-helper.js +8 -8
  79. data/client-app/tests/unit/controllers/index-test.js +12 -12
  80. data/client-app/tests/unit/controllers/show-test.js +12 -12
  81. data/client-app/tests/unit/initializers/app-init-test.js +31 -31
  82. data/client-app/tests/unit/routes/index-test.js +11 -11
  83. data/client-app/tests/unit/routes/show-test.js +11 -11
  84. data/lib/examples/sidekiq_logster_reporter.rb +21 -21
  85. data/lib/logster.rb +54 -54
  86. data/lib/logster/base_store.rb +130 -130
  87. data/lib/logster/configuration.rb +25 -25
  88. data/lib/logster/ignore_pattern.rb +65 -65
  89. data/lib/logster/logger.rb +108 -102
  90. data/lib/logster/message.rb +227 -227
  91. data/lib/logster/middleware/debug_exceptions.rb +26 -26
  92. data/lib/logster/middleware/reporter.rb +56 -56
  93. data/lib/logster/middleware/viewer.rb +220 -220
  94. data/lib/logster/rails/railtie.rb +58 -58
  95. data/lib/logster/redis_store.rb +481 -481
  96. data/lib/logster/version.rb +3 -3
  97. data/lib/logster/web.rb +14 -14
  98. data/logster.gemspec +34 -34
  99. data/test/examples/test_sidekiq_reporter_example.rb +46 -46
  100. data/test/fake_data/Gemfile +4 -4
  101. data/test/fake_data/generate.rb +10 -10
  102. data/test/logster/middleware/test_reporter.rb +21 -21
  103. data/test/logster/middleware/test_viewer.rb +96 -96
  104. data/test/logster/test_base_store.rb +147 -147
  105. data/test/logster/test_ignore_pattern.rb +41 -41
  106. data/test/logster/test_logger.rb +80 -74
  107. data/test/logster/test_message.rb +34 -34
  108. data/test/logster/test_redis_rate_limiter.rb +230 -230
  109. data/test/logster/test_redis_store.rb +427 -427
  110. data/test/test_helper.rb +38 -38
  111. data/vendor/assets/javascripts/logster.js.erb +39 -39
  112. metadata +3 -3
@@ -1,58 +1,58 @@
1
- module Logster::Rails
2
-
3
- # this magically registers logster.js in the asset pipeline
4
- class Engine < Rails::Engine
5
- end
6
-
7
- def self.set_logger(config)
8
- return unless Logster.config.environments.include?(Rails.env.to_sym)
9
-
10
- require 'logster/middleware/debug_exceptions'
11
- require 'logster/middleware/reporter'
12
-
13
- store = Logster.store ||= Logster::RedisStore.new
14
- store.level = Logger::Severity::WARN if Rails.env.production?
15
-
16
- logger = Logster::Logger.new(store)
17
- logger.chain(::Rails.logger)
18
- logger.level = ::Rails.logger.level
19
-
20
- Logster.logger = ::Rails.logger = config.logger = logger
21
- end
22
-
23
-
24
- def self.initialize!(app)
25
- return unless Logster.config.environments.include?(Rails.env.to_sym)
26
-
27
- if Logster::Logger === Rails.logger
28
- app.middleware.insert_before ActionDispatch::ShowExceptions, Logster::Middleware::Reporter
29
-
30
- if Rails::VERSION::MAJOR == 3
31
- app.middleware.insert_before ActionDispatch::DebugExceptions, Logster::Middleware::DebugExceptions
32
- else
33
- app.middleware.insert_before ActionDispatch::DebugExceptions, Logster::Middleware::DebugExceptions, Rails.application
34
- end
35
-
36
- app.middleware.delete ActionDispatch::DebugExceptions
37
- app.config.colorize_logging = false
38
-
39
- unless Logster.config.application_version
40
- git_version = `cd #{Rails.root} && git rev-parse --short HEAD 2> /dev/null`
41
- if git_version.present?
42
- Logster.config.application_version = git_version.strip
43
- end
44
- end
45
- end
46
- end
47
-
48
- class Railtie < ::Rails::Railtie
49
-
50
- config.before_initialize do
51
- Logster::Rails.set_logger(config)
52
- end
53
-
54
- initializer "logster.configure_rails_initialization" do |app|
55
- Logster::Rails.initialize!(app)
56
- end
57
- end
58
- end
1
+ module Logster::Rails
2
+
3
+ # this magically registers logster.js in the asset pipeline
4
+ class Engine < Rails::Engine
5
+ end
6
+
7
+ def self.set_logger(config)
8
+ return unless Logster.config.environments.include?(Rails.env.to_sym)
9
+
10
+ require 'logster/middleware/debug_exceptions'
11
+ require 'logster/middleware/reporter'
12
+
13
+ store = Logster.store ||= Logster::RedisStore.new
14
+ store.level = Logger::Severity::WARN if Rails.env.production?
15
+
16
+ logger = Logster::Logger.new(store)
17
+ logger.chain(::Rails.logger)
18
+ logger.level = ::Rails.logger.level
19
+
20
+ Logster.logger = ::Rails.logger = config.logger = logger
21
+ end
22
+
23
+
24
+ def self.initialize!(app)
25
+ return unless Logster.config.environments.include?(Rails.env.to_sym)
26
+
27
+ if Logster::Logger === Rails.logger
28
+ app.middleware.insert_before ActionDispatch::ShowExceptions, Logster::Middleware::Reporter
29
+
30
+ if Rails::VERSION::MAJOR == 3
31
+ app.middleware.insert_before ActionDispatch::DebugExceptions, Logster::Middleware::DebugExceptions
32
+ else
33
+ app.middleware.insert_before ActionDispatch::DebugExceptions, Logster::Middleware::DebugExceptions, Rails.application
34
+ end
35
+
36
+ app.middleware.delete ActionDispatch::DebugExceptions
37
+ app.config.colorize_logging = false
38
+
39
+ unless Logster.config.application_version
40
+ git_version = `cd #{Rails.root} && git rev-parse --short HEAD 2> /dev/null`
41
+ if git_version.present?
42
+ Logster.config.application_version = git_version.strip
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ class Railtie < ::Rails::Railtie
49
+
50
+ config.before_initialize do
51
+ Logster::Rails.set_logger(config)
52
+ end
53
+
54
+ initializer "logster.configure_rails_initialization" do |app|
55
+ Logster::Rails.initialize!(app)
56
+ end
57
+ end
58
+ end
@@ -1,481 +1,481 @@
1
- require 'json'
2
- require 'logster/base_store'
3
-
4
- module Logster
5
- class RedisRateLimiter
6
- BUCKETS = 6
7
- PREFIX = "__LOGSTER__RATE_LIMIT".freeze
8
-
9
- attr_reader :duration, :callback
10
-
11
- def self.clear_all(redis, redis_prefix=nil)
12
- prefix = key_prefix(redis_prefix)
13
-
14
- redis.eval "
15
- local keys = redis.call('keys', '*#{prefix}*')
16
- if (table.getn(keys) > 0) then
17
- redis.call('del', unpack(keys))
18
- end
19
- "
20
- end
21
-
22
- def initialize(redis, severities, limit, duration, redis_prefix = nil, callback = nil)
23
- @severities = severities
24
- @limit = limit
25
- @duration = duration
26
- @callback = callback
27
- @redis_prefix = redis_prefix
28
- @redis = redis
29
- @bucket_range = @duration / BUCKETS
30
- @mget_keys = (0..(BUCKETS - 1)).map { |i| "#{key}:#{i}" }
31
- end
32
-
33
- def retrieve_rate
34
- @redis.mget(@mget_keys).reduce(0) { |sum, value| sum + value.to_i }
35
- end
36
-
37
- def check(severity)
38
- return unless @severities.include?(severity)
39
- time = Time.now.to_i
40
- num = bucket_number(time)
41
- redis_key = "#{key}:#{num}"
42
-
43
- current_rate = @redis.eval <<-LUA
44
- local bucket_number = #{num}
45
- local bucket_count = redis.call("INCR", "#{redis_key}")
46
-
47
- if bucket_count == 1 then
48
- redis.call("EXPIRE", "#{redis_key}", "#{bucket_expiry(time)}")
49
- redis.call("DEL", "#{callback_key}")
50
- end
51
-
52
- local function retrieve_rate ()
53
- local sum = 0
54
- local values = redis.call("MGET", #{mget_keys(num)})
55
- for index, value in ipairs(values) do
56
- if value ~= false then sum = sum + value end
57
- end
58
- return sum
59
- end
60
-
61
- return (retrieve_rate() + bucket_count)
62
- LUA
63
-
64
- if !@redis.get(callback_key) && (current_rate >= @limit)
65
- @callback.call(current_rate) if @callback
66
- @redis.set(callback_key, 1)
67
- end
68
-
69
- current_rate
70
- end
71
-
72
- def key
73
- # "_LOGSTER_RATE_LIMIT:012:20:30"
74
- # Triggers callback when log levels of :debug, :info and :warn occurs 20 times within 30 secs
75
- "#{key_prefix}:#{@severities.join("")}:#{@limit}:#{@duration}"
76
- end
77
-
78
- def callback_key
79
- "#{key}:callback_triggered"
80
- end
81
-
82
- private
83
-
84
- def self.key_prefix(redis_prefix)
85
- if redis_prefix
86
- "#{redis_prefix.call}:#{PREFIX}"
87
- else
88
- PREFIX
89
- end
90
-
91
- end
92
-
93
- def key_prefix
94
- self.class.key_prefix(@redis_prefix)
95
- end
96
-
97
- def mget_keys(bucket_num)
98
- keys = @mget_keys.dup
99
- keys.delete_at(bucket_num)
100
- keys.map { |key| "'#{key}'" }.join(', ')
101
- end
102
-
103
- def bucket_number(time)
104
- (time % @duration) / @bucket_range
105
- end
106
-
107
- def bucket_expiry(time)
108
- @duration - ((time % @duration) % @bucket_range)
109
- end
110
- end
111
-
112
- class RedisStore < BaseStore
113
-
114
- attr_accessor :redis, :max_backlog, :redis_raw_connection
115
- attr_writer :redis_prefix
116
-
117
- def initialize(redis = nil)
118
- super()
119
- @redis = redis || Redis.new
120
- @max_backlog = 1000
121
- @redis_prefix = nil
122
- @redis_raw_connection = nil
123
- end
124
-
125
- def save(message)
126
- if keys=message.solved_keys
127
- keys.each do |solved|
128
- return true if @redis.hget(solved_key, solved)
129
- end
130
- end
131
-
132
- @redis.multi do
133
- @redis.hset(grouping_key, message.grouping_key, message.key)
134
- @redis.rpush(list_key, message.key)
135
- update_message(message)
136
- end
137
-
138
- trim
139
- check_rate_limits(message.severity)
140
-
141
- true
142
- end
143
-
144
- def delete(msg)
145
- @redis.multi do
146
- @redis.hdel(hash_key, msg.key)
147
- @redis.hdel(grouping_key, msg.grouping_key)
148
- @redis.lrem(list_key, -1, msg.key)
149
- end
150
- end
151
-
152
- def replace_and_bump(message)
153
- # TODO make it atomic
154
- exists = @redis.hexists(hash_key, message.key)
155
- return false unless exists
156
-
157
- @redis.multi do
158
- @redis.hset(hash_key, message.key, message.to_json)
159
- @redis.lrem(list_key, -1, message.key)
160
- @redis.rpush(list_key, message.key)
161
- end
162
-
163
- check_rate_limits(message.severity)
164
-
165
- true
166
- end
167
-
168
- def similar_key(message)
169
- @redis.hget(grouping_key, message.grouping_key)
170
- end
171
-
172
- def count
173
- @redis.llen(list_key)
174
- end
175
-
176
- def solve(message_key)
177
- if (message = get(message_key)) && (keys = message.solved_keys)
178
- # add a time so we can expire it
179
- keys.each do |s_key|
180
- @redis.hset(solved_key, s_key, Time.now.to_f.to_i)
181
- end
182
- end
183
- clear_solved
184
- end
185
-
186
- def latest(opts={})
187
- limit = opts[:limit] || 50
188
- severity = opts[:severity]
189
- before = opts[:before]
190
- after = opts[:after]
191
- search = opts[:search]
192
-
193
- start, finish = find_location(before, after, limit)
194
-
195
- return [] unless start && finish
196
-
197
- results = []
198
-
199
- direction = after ? 1 : -1
200
-
201
- begin
202
- keys = @redis.lrange(list_key, start, finish) || []
203
- break unless keys and keys.count > 0
204
- rows = @redis.hmget(hash_key, keys)
205
-
206
- temp = []
207
-
208
- rows.each do |s|
209
- row = Message.from_json(s)
210
- break if before && before == row.key
211
- row = nil if severity && !severity.include?(row.severity)
212
-
213
- row = filter_search(row, search)
214
- temp << row if row
215
- end
216
-
217
- if direction == -1
218
- results = temp + results
219
- else
220
- results += temp
221
- end
222
-
223
- start += limit * direction
224
- finish += limit * direction
225
-
226
- finish = -1 if finish > -1
227
- end while rows.length > 0 && results.length < limit && start < 0
228
-
229
- results
230
- end
231
-
232
- def clear
233
- RedisRateLimiter.clear_all(@redis)
234
- @redis.del(solved_key)
235
- @redis.del(list_key)
236
- keys = @redis.smembers(protected_key) || []
237
- if keys.empty?
238
- @redis.del(hash_key)
239
- else
240
- protected = @redis.mapped_hmget(hash_key, *keys)
241
- @redis.del(hash_key)
242
- @redis.mapped_hmset(hash_key, protected)
243
-
244
- sorted = protected
245
- .values
246
- .map { |string|
247
- Message.from_json(string) rescue nil
248
- }
249
- .compact
250
- .sort
251
- .map(&:key)
252
-
253
- @redis.pipelined do
254
- sorted.each do |message_key|
255
- @redis.rpush(list_key, message_key)
256
- end
257
- end
258
- end
259
- end
260
-
261
- # Delete everything, included protected messages
262
- # (use in tests)
263
- def clear_all
264
- @redis.del(list_key)
265
- @redis.del(protected_key)
266
- @redis.del(hash_key)
267
- @redis.del(grouping_key)
268
- @redis.del(solved_key)
269
- end
270
-
271
- def get(message_key)
272
- json = @redis.hget(hash_key, message_key)
273
- return nil unless json
274
-
275
- Message.from_json(json)
276
- end
277
-
278
- def protect(message_key)
279
- if message = get(message_key)
280
- message.protected = true
281
- update_message(message)
282
- end
283
- end
284
-
285
- def unprotect(message_key)
286
- if message = get(message_key)
287
- message.protected = false
288
- update_message(message)
289
- else
290
- raise "Message already deleted"
291
- end
292
- end
293
-
294
- def solved
295
- @redis.hkeys(solved_key) || []
296
- end
297
-
298
- def register_rate_limit_per_minute(severities, limit, &block)
299
- register_rate_limit(severities, limit, 60, block)
300
- end
301
-
302
- def register_rate_limit_per_hour(severities, limit, &block)
303
- register_rate_limit(severities, limit, 3600, block)
304
- end
305
-
306
- def redis_prefix
307
- return 'default'.freeze if !@redis_prefix
308
- @prefix_is_proc ||= @redis_prefix.respond_to?(:call)
309
- @prefix_is_proc ? @redis_prefix.call : @redis_prefix
310
- end
311
-
312
- def rate_limits
313
- @rate_limits ||= {}
314
- end
315
-
316
- protected
317
-
318
- def clear_solved(count = nil)
319
-
320
- ignores = Set.new(@redis.hkeys(solved_key) || [])
321
-
322
- if ignores.length > 0
323
- start = count ? 0 - count : 0
324
- message_keys = @redis.lrange(list_key, start, -1 ) || []
325
-
326
- @redis.hmget(hash_key, message_keys).each do |json|
327
- message = Message.from_json(json)
328
- unless (ignores & (message.solved_keys || [])).empty?
329
- delete message
330
- end
331
- end
332
- end
333
- end
334
-
335
- def trim
336
- if @redis.llen(list_key) > max_backlog
337
- removed_keys = []
338
- while removed_key = @redis.lpop(list_key)
339
- unless @redis.sismember(protected_key, removed_key)
340
- rmsg = get removed_key
341
- @redis.hdel(hash_key, rmsg.key)
342
- @redis.hdel(grouping_key, rmsg.grouping_key)
343
- break
344
- else
345
- removed_keys << removed_key
346
- end
347
- end
348
- removed_keys.reverse.each do |key|
349
- @redis.lpush(list_key, key)
350
- end
351
- end
352
- end
353
-
354
- def update_message(message)
355
- @redis.hset(hash_key, message.key, message.to_json)
356
- if message.protected
357
- @redis.sadd(protected_key, message.key)
358
- else
359
- @redis.srem(protected_key, message.key)
360
- end
361
- end
362
-
363
- def find_message(list, message_key)
364
- limit = 50
365
- start = 0
366
- finish = limit - 1
367
-
368
- found = nil
369
- while found == nil
370
- items = @redis.lrange(list, start, finish)
371
-
372
- break unless items && items.length > 0
373
-
374
- found = items.index(message_key)
375
- break if found
376
-
377
- start += limit
378
- finish += limit
379
- end
380
-
381
- found
382
- end
383
-
384
- def find_location(before, after, limit)
385
- start = -limit
386
- finish = -1
387
-
388
- return [start,finish] unless before || after
389
-
390
- found = nil
391
- find = before || after
392
-
393
- while !found
394
- items = @redis.lrange(list_key, start, finish)
395
-
396
- break unless items && items.length > 0
397
-
398
- found = items.index(find)
399
-
400
- if items.length < limit
401
- found += limit - items.length if found
402
- break
403
- end
404
- break if found
405
- start -= limit
406
- finish -= limit
407
- end
408
-
409
- if found
410
- if before
411
- offset = -(limit - found)
412
- else
413
- offset = found + 1
414
- end
415
-
416
- start += offset
417
- finish += offset
418
-
419
- finish = -1 if finish > -1
420
- return nil if start > -1
421
- end
422
-
423
- [start, finish]
424
- end
425
-
426
- def filter_search(row, search)
427
- return row unless row && search
428
-
429
- if Regexp === search
430
- row if row.message =~ search
431
- elsif search[0] == "-"
432
- exclude = search.sub('-', '')
433
- row unless row.message.include?(exclude)
434
- elsif row.message.include?(search)
435
- row
436
- end
437
-
438
- end
439
-
440
- def check_rate_limits(severity)
441
- rate_limits_to_check = rate_limits[self.redis_prefix]
442
- return if !rate_limits_to_check
443
- rate_limits_to_check.each { |rate_limit| rate_limit.check(severity) }
444
- end
445
-
446
- def solved_key
447
- @solved_key ||= "__LOGSTER__SOLVED_MAP"
448
- end
449
-
450
- def list_key
451
- @list_key ||= "__LOGSTER__LATEST"
452
- end
453
-
454
- def hash_key
455
- @hash_key ||= "__LOGSTER__MAP"
456
- end
457
-
458
- def protected_key
459
- @saved_key ||= "__LOGSTER__SAVED"
460
- end
461
-
462
- def grouping_key
463
- @grouping_key ||= "__LOGSTER__GMAP"
464
- end
465
-
466
- private
467
-
468
- def register_rate_limit(severities, limit, duration, callback)
469
- severities = [severities] unless severities.is_a?(Array)
470
- redis = (@redis_raw_connection && @redis_prefix) ? @redis_raw_connection : @redis
471
-
472
- rate_limiter = RedisRateLimiter.new(
473
- redis, severities, limit, duration, Proc.new { redis_prefix }, callback
474
- )
475
-
476
- rate_limits[self.redis_prefix] ||= []
477
- rate_limits[self.redis_prefix] << rate_limiter
478
- rate_limiter
479
- end
480
- end
481
- end
1
+ require 'json'
2
+ require 'logster/base_store'
3
+
4
+ module Logster
5
+ class RedisRateLimiter
6
+ BUCKETS = 6
7
+ PREFIX = "__LOGSTER__RATE_LIMIT".freeze
8
+
9
+ attr_reader :duration, :callback
10
+
11
+ def self.clear_all(redis, redis_prefix=nil)
12
+ prefix = key_prefix(redis_prefix)
13
+
14
+ redis.eval "
15
+ local keys = redis.call('keys', '*#{prefix}*')
16
+ if (table.getn(keys) > 0) then
17
+ redis.call('del', unpack(keys))
18
+ end
19
+ "
20
+ end
21
+
22
+ def initialize(redis, severities, limit, duration, redis_prefix = nil, callback = nil)
23
+ @severities = severities
24
+ @limit = limit
25
+ @duration = duration
26
+ @callback = callback
27
+ @redis_prefix = redis_prefix
28
+ @redis = redis
29
+ @bucket_range = @duration / BUCKETS
30
+ @mget_keys = (0..(BUCKETS - 1)).map { |i| "#{key}:#{i}" }
31
+ end
32
+
33
+ def retrieve_rate
34
+ @redis.mget(@mget_keys).reduce(0) { |sum, value| sum + value.to_i }
35
+ end
36
+
37
+ def check(severity)
38
+ return unless @severities.include?(severity)
39
+ time = Time.now.to_i
40
+ num = bucket_number(time)
41
+ redis_key = "#{key}:#{num}"
42
+
43
+ current_rate = @redis.eval <<-LUA
44
+ local bucket_number = #{num}
45
+ local bucket_count = redis.call("INCR", "#{redis_key}")
46
+
47
+ if bucket_count == 1 then
48
+ redis.call("EXPIRE", "#{redis_key}", "#{bucket_expiry(time)}")
49
+ redis.call("DEL", "#{callback_key}")
50
+ end
51
+
52
+ local function retrieve_rate ()
53
+ local sum = 0
54
+ local values = redis.call("MGET", #{mget_keys(num)})
55
+ for index, value in ipairs(values) do
56
+ if value ~= false then sum = sum + value end
57
+ end
58
+ return sum
59
+ end
60
+
61
+ return (retrieve_rate() + bucket_count)
62
+ LUA
63
+
64
+ if !@redis.get(callback_key) && (current_rate >= @limit)
65
+ @callback.call(current_rate) if @callback
66
+ @redis.set(callback_key, 1)
67
+ end
68
+
69
+ current_rate
70
+ end
71
+
72
+ def key
73
+ # "_LOGSTER_RATE_LIMIT:012:20:30"
74
+ # Triggers callback when log levels of :debug, :info and :warn occurs 20 times within 30 secs
75
+ "#{key_prefix}:#{@severities.join("")}:#{@limit}:#{@duration}"
76
+ end
77
+
78
+ def callback_key
79
+ "#{key}:callback_triggered"
80
+ end
81
+
82
+ private
83
+
84
+ def self.key_prefix(redis_prefix)
85
+ if redis_prefix
86
+ "#{redis_prefix.call}:#{PREFIX}"
87
+ else
88
+ PREFIX
89
+ end
90
+
91
+ end
92
+
93
+ def key_prefix
94
+ self.class.key_prefix(@redis_prefix)
95
+ end
96
+
97
+ def mget_keys(bucket_num)
98
+ keys = @mget_keys.dup
99
+ keys.delete_at(bucket_num)
100
+ keys.map { |key| "'#{key}'" }.join(', ')
101
+ end
102
+
103
+ def bucket_number(time)
104
+ (time % @duration) / @bucket_range
105
+ end
106
+
107
+ def bucket_expiry(time)
108
+ @duration - ((time % @duration) % @bucket_range)
109
+ end
110
+ end
111
+
112
+ class RedisStore < BaseStore
113
+
114
+ attr_accessor :redis, :max_backlog, :redis_raw_connection
115
+ attr_writer :redis_prefix
116
+
117
+ def initialize(redis = nil)
118
+ super()
119
+ @redis = redis || Redis.new
120
+ @max_backlog = 1000
121
+ @redis_prefix = nil
122
+ @redis_raw_connection = nil
123
+ end
124
+
125
+ def save(message)
126
+ if keys=message.solved_keys
127
+ keys.each do |solved|
128
+ return true if @redis.hget(solved_key, solved)
129
+ end
130
+ end
131
+
132
+ @redis.multi do
133
+ @redis.hset(grouping_key, message.grouping_key, message.key)
134
+ @redis.rpush(list_key, message.key)
135
+ update_message(message)
136
+ end
137
+
138
+ trim
139
+ check_rate_limits(message.severity)
140
+
141
+ true
142
+ end
143
+
144
+ def delete(msg)
145
+ @redis.multi do
146
+ @redis.hdel(hash_key, msg.key)
147
+ @redis.hdel(grouping_key, msg.grouping_key)
148
+ @redis.lrem(list_key, -1, msg.key)
149
+ end
150
+ end
151
+
152
+ def replace_and_bump(message)
153
+ # TODO make it atomic
154
+ exists = @redis.hexists(hash_key, message.key)
155
+ return false unless exists
156
+
157
+ @redis.multi do
158
+ @redis.hset(hash_key, message.key, message.to_json)
159
+ @redis.lrem(list_key, -1, message.key)
160
+ @redis.rpush(list_key, message.key)
161
+ end
162
+
163
+ check_rate_limits(message.severity)
164
+
165
+ true
166
+ end
167
+
168
+ def similar_key(message)
169
+ @redis.hget(grouping_key, message.grouping_key)
170
+ end
171
+
172
+ def count
173
+ @redis.llen(list_key)
174
+ end
175
+
176
+ def solve(message_key)
177
+ if (message = get(message_key)) && (keys = message.solved_keys)
178
+ # add a time so we can expire it
179
+ keys.each do |s_key|
180
+ @redis.hset(solved_key, s_key, Time.now.to_f.to_i)
181
+ end
182
+ end
183
+ clear_solved
184
+ end
185
+
186
+ def latest(opts={})
187
+ limit = opts[:limit] || 50
188
+ severity = opts[:severity]
189
+ before = opts[:before]
190
+ after = opts[:after]
191
+ search = opts[:search]
192
+
193
+ start, finish = find_location(before, after, limit)
194
+
195
+ return [] unless start && finish
196
+
197
+ results = []
198
+
199
+ direction = after ? 1 : -1
200
+
201
+ begin
202
+ keys = @redis.lrange(list_key, start, finish) || []
203
+ break unless keys and keys.count > 0
204
+ rows = @redis.hmget(hash_key, keys)
205
+
206
+ temp = []
207
+
208
+ rows.each do |s|
209
+ row = Message.from_json(s)
210
+ break if before && before == row.key
211
+ row = nil if severity && !severity.include?(row.severity)
212
+
213
+ row = filter_search(row, search)
214
+ temp << row if row
215
+ end
216
+
217
+ if direction == -1
218
+ results = temp + results
219
+ else
220
+ results += temp
221
+ end
222
+
223
+ start += limit * direction
224
+ finish += limit * direction
225
+
226
+ finish = -1 if finish > -1
227
+ end while rows.length > 0 && results.length < limit && start < 0
228
+
229
+ results
230
+ end
231
+
232
+ def clear
233
+ RedisRateLimiter.clear_all(@redis)
234
+ @redis.del(solved_key)
235
+ @redis.del(list_key)
236
+ keys = @redis.smembers(protected_key) || []
237
+ if keys.empty?
238
+ @redis.del(hash_key)
239
+ else
240
+ protected = @redis.mapped_hmget(hash_key, *keys)
241
+ @redis.del(hash_key)
242
+ @redis.mapped_hmset(hash_key, protected)
243
+
244
+ sorted = protected
245
+ .values
246
+ .map { |string|
247
+ Message.from_json(string) rescue nil
248
+ }
249
+ .compact
250
+ .sort
251
+ .map(&:key)
252
+
253
+ @redis.pipelined do
254
+ sorted.each do |message_key|
255
+ @redis.rpush(list_key, message_key)
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ # Delete everything, included protected messages
262
+ # (use in tests)
263
+ def clear_all
264
+ @redis.del(list_key)
265
+ @redis.del(protected_key)
266
+ @redis.del(hash_key)
267
+ @redis.del(grouping_key)
268
+ @redis.del(solved_key)
269
+ end
270
+
271
+ def get(message_key)
272
+ json = @redis.hget(hash_key, message_key)
273
+ return nil unless json
274
+
275
+ Message.from_json(json)
276
+ end
277
+
278
+ def protect(message_key)
279
+ if message = get(message_key)
280
+ message.protected = true
281
+ update_message(message)
282
+ end
283
+ end
284
+
285
+ def unprotect(message_key)
286
+ if message = get(message_key)
287
+ message.protected = false
288
+ update_message(message)
289
+ else
290
+ raise "Message already deleted"
291
+ end
292
+ end
293
+
294
+ def solved
295
+ @redis.hkeys(solved_key) || []
296
+ end
297
+
298
+ def register_rate_limit_per_minute(severities, limit, &block)
299
+ register_rate_limit(severities, limit, 60, block)
300
+ end
301
+
302
+ def register_rate_limit_per_hour(severities, limit, &block)
303
+ register_rate_limit(severities, limit, 3600, block)
304
+ end
305
+
306
+ def redis_prefix
307
+ return 'default'.freeze if !@redis_prefix
308
+ @prefix_is_proc ||= @redis_prefix.respond_to?(:call)
309
+ @prefix_is_proc ? @redis_prefix.call : @redis_prefix
310
+ end
311
+
312
+ def rate_limits
313
+ @rate_limits ||= {}
314
+ end
315
+
316
+ protected
317
+
318
+ def clear_solved(count = nil)
319
+
320
+ ignores = Set.new(@redis.hkeys(solved_key) || [])
321
+
322
+ if ignores.length > 0
323
+ start = count ? 0 - count : 0
324
+ message_keys = @redis.lrange(list_key, start, -1 ) || []
325
+
326
+ @redis.hmget(hash_key, message_keys).each do |json|
327
+ message = Message.from_json(json)
328
+ unless (ignores & (message.solved_keys || [])).empty?
329
+ delete message
330
+ end
331
+ end
332
+ end
333
+ end
334
+
335
+ def trim
336
+ if @redis.llen(list_key) > max_backlog
337
+ removed_keys = []
338
+ while removed_key = @redis.lpop(list_key)
339
+ unless @redis.sismember(protected_key, removed_key)
340
+ rmsg = get removed_key
341
+ @redis.hdel(hash_key, rmsg.key)
342
+ @redis.hdel(grouping_key, rmsg.grouping_key)
343
+ break
344
+ else
345
+ removed_keys << removed_key
346
+ end
347
+ end
348
+ removed_keys.reverse.each do |key|
349
+ @redis.lpush(list_key, key)
350
+ end
351
+ end
352
+ end
353
+
354
+ def update_message(message)
355
+ @redis.hset(hash_key, message.key, message.to_json)
356
+ if message.protected
357
+ @redis.sadd(protected_key, message.key)
358
+ else
359
+ @redis.srem(protected_key, message.key)
360
+ end
361
+ end
362
+
363
+ def find_message(list, message_key)
364
+ limit = 50
365
+ start = 0
366
+ finish = limit - 1
367
+
368
+ found = nil
369
+ while found == nil
370
+ items = @redis.lrange(list, start, finish)
371
+
372
+ break unless items && items.length > 0
373
+
374
+ found = items.index(message_key)
375
+ break if found
376
+
377
+ start += limit
378
+ finish += limit
379
+ end
380
+
381
+ found
382
+ end
383
+
384
+ def find_location(before, after, limit)
385
+ start = -limit
386
+ finish = -1
387
+
388
+ return [start,finish] unless before || after
389
+
390
+ found = nil
391
+ find = before || after
392
+
393
+ while !found
394
+ items = @redis.lrange(list_key, start, finish)
395
+
396
+ break unless items && items.length > 0
397
+
398
+ found = items.index(find)
399
+
400
+ if items.length < limit
401
+ found += limit - items.length if found
402
+ break
403
+ end
404
+ break if found
405
+ start -= limit
406
+ finish -= limit
407
+ end
408
+
409
+ if found
410
+ if before
411
+ offset = -(limit - found)
412
+ else
413
+ offset = found + 1
414
+ end
415
+
416
+ start += offset
417
+ finish += offset
418
+
419
+ finish = -1 if finish > -1
420
+ return nil if start > -1
421
+ end
422
+
423
+ [start, finish]
424
+ end
425
+
426
+ def filter_search(row, search)
427
+ return row unless row && search
428
+
429
+ if Regexp === search
430
+ row if row.message =~ search
431
+ elsif search[0] == "-"
432
+ exclude = search.sub('-', '')
433
+ row unless row.message.include?(exclude)
434
+ elsif row.message.include?(search)
435
+ row
436
+ end
437
+
438
+ end
439
+
440
+ def check_rate_limits(severity)
441
+ rate_limits_to_check = rate_limits[self.redis_prefix]
442
+ return if !rate_limits_to_check
443
+ rate_limits_to_check.each { |rate_limit| rate_limit.check(severity) }
444
+ end
445
+
446
+ def solved_key
447
+ @solved_key ||= "__LOGSTER__SOLVED_MAP"
448
+ end
449
+
450
+ def list_key
451
+ @list_key ||= "__LOGSTER__LATEST"
452
+ end
453
+
454
+ def hash_key
455
+ @hash_key ||= "__LOGSTER__MAP"
456
+ end
457
+
458
+ def protected_key
459
+ @saved_key ||= "__LOGSTER__SAVED"
460
+ end
461
+
462
+ def grouping_key
463
+ @grouping_key ||= "__LOGSTER__GMAP"
464
+ end
465
+
466
+ private
467
+
468
+ def register_rate_limit(severities, limit, duration, callback)
469
+ severities = [severities] unless severities.is_a?(Array)
470
+ redis = (@redis_raw_connection && @redis_prefix) ? @redis_raw_connection : @redis
471
+
472
+ rate_limiter = RedisRateLimiter.new(
473
+ redis, severities, limit, duration, Proc.new { redis_prefix }, callback
474
+ )
475
+
476
+ rate_limits[self.redis_prefix] ||= []
477
+ rate_limits[self.redis_prefix] << rate_limiter
478
+ rate_limiter
479
+ end
480
+ end
481
+ end