logster 1.2.11 → 1.3.pre

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