logster 2.1.2 → 2.2.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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +21 -19
  3. data/.rubocop.yml +1 -1
  4. data/.travis.yml +16 -16
  5. data/CHANGELOG.md +224 -172
  6. data/Gemfile +4 -4
  7. data/Guardfile +8 -8
  8. data/LICENSE.txt +22 -22
  9. data/README.md +99 -99
  10. data/Rakefile +21 -21
  11. data/assets/fonts/FontAwesome.otf +0 -0
  12. data/assets/fonts/fontawesome-webfont.eot +0 -0
  13. data/assets/fonts/fontawesome-webfont.svg +639 -639
  14. data/assets/fonts/fontawesome-webfont.ttf +0 -0
  15. data/assets/fonts/fontawesome-webfont.woff +0 -0
  16. data/assets/fonts/fontawesome-webfont.woff2 +0 -0
  17. data/assets/images/Icon-144_rounded.png +0 -0
  18. data/assets/images/Icon-144_square.png +0 -0
  19. data/assets/images/icon_144x144.png +0 -0
  20. data/assets/images/icon_64x64.png +0 -0
  21. data/assets/javascript/client-app.js +115 -106
  22. data/assets/stylesheets/client-app.css +1 -1
  23. data/build_client_app.sh +0 -0
  24. data/client-app/.editorconfig +20 -20
  25. data/client-app/.ember-cli +9 -9
  26. data/client-app/.eslintignore +19 -19
  27. data/client-app/.eslintrc.js +46 -46
  28. data/client-app/.gitignore +23 -23
  29. data/client-app/.travis.yml +27 -27
  30. data/client-app/.watchmanconfig +3 -3
  31. data/client-app/README.md +57 -57
  32. data/client-app/app/app.js +0 -0
  33. data/client-app/app/components/actions-menu.js +43 -43
  34. data/client-app/app/components/env-tab.js +80 -80
  35. data/client-app/app/components/message-info.js +0 -0
  36. data/client-app/app/components/message-row.js +0 -0
  37. data/client-app/app/components/panel-resizer.js +0 -0
  38. data/client-app/app/components/patterns-list.js +109 -0
  39. data/client-app/app/components/tab-contents.js +27 -27
  40. data/client-app/app/components/tabbed-section.js +0 -0
  41. data/client-app/app/components/time-formatter.js +0 -0
  42. data/client-app/app/components/update-time.js +0 -0
  43. data/client-app/app/controllers/index.js +22 -6
  44. data/client-app/app/controllers/show.js +0 -0
  45. data/client-app/app/helpers/logster-url.js +12 -0
  46. data/client-app/app/helpers/or.js +7 -0
  47. data/client-app/app/index.html +30 -29
  48. data/client-app/app/initializers/app-init.js +67 -67
  49. data/client-app/app/lib/preload.js +20 -20
  50. data/client-app/app/lib/utilities.js +150 -149
  51. data/client-app/app/models/message-collection.js +0 -0
  52. data/client-app/app/models/message.js +100 -100
  53. data/client-app/app/models/pattern-item.js +25 -0
  54. data/client-app/app/resolver.js +0 -0
  55. data/client-app/app/router.js +1 -0
  56. data/client-app/app/routes/index.js +2 -9
  57. data/client-app/app/routes/settings.js +15 -0
  58. data/client-app/app/routes/show.js +0 -0
  59. data/client-app/app/styles/app.css +633 -527
  60. data/client-app/app/templates/application.hbs +2 -2
  61. data/client-app/app/templates/components/actions-menu.hbs +12 -12
  62. data/client-app/app/templates/components/env-tab.hbs +10 -10
  63. data/client-app/app/templates/components/message-info.hbs +41 -41
  64. data/client-app/app/templates/components/message-row.hbs +15 -15
  65. data/client-app/app/templates/components/panel-resizer.hbs +3 -3
  66. data/client-app/app/templates/components/patterns-list.hbs +25 -0
  67. data/client-app/app/templates/components/tabbed-section.hbs +10 -10
  68. data/client-app/app/templates/components/time-formatter.hbs +1 -1
  69. data/client-app/app/templates/index.hbs +65 -58
  70. data/client-app/app/templates/settings.hbs +26 -0
  71. data/client-app/app/templates/show.hbs +7 -7
  72. data/client-app/config/environment.js +51 -51
  73. data/client-app/config/optional-features.json +3 -3
  74. data/client-app/config/targets.js +18 -18
  75. data/client-app/ember-cli-build.js +29 -29
  76. data/client-app/package-lock.json +11357 -11365
  77. data/client-app/package.json +57 -56
  78. data/client-app/public/assets/images/icon_144x144.png +0 -0
  79. data/client-app/public/assets/images/icon_64x64.png +0 -0
  80. data/client-app/testem.js +25 -25
  81. data/client-app/tests/index.html +34 -34
  82. data/client-app/tests/integration/components/env-tab-test.js +134 -123
  83. data/client-app/tests/integration/components/message-info-test.js +111 -111
  84. data/client-app/tests/integration/components/patterns-list-test.js +56 -0
  85. data/client-app/tests/test-helper.js +8 -8
  86. data/client-app/tests/unit/controllers/index-test.js +12 -12
  87. data/client-app/tests/unit/controllers/show-test.js +12 -12
  88. data/client-app/tests/unit/initializers/app-init-test.js +31 -31
  89. data/client-app/tests/unit/routes/index-test.js +11 -11
  90. data/client-app/tests/unit/routes/show-test.js +11 -11
  91. data/lib/examples/sidekiq_logster_reporter.rb +21 -21
  92. data/lib/logster.rb +59 -54
  93. data/lib/logster/base_store.rb +169 -141
  94. data/lib/logster/cache.rb +20 -0
  95. data/lib/logster/configuration.rb +27 -26
  96. data/lib/logster/defer_logger.rb +14 -14
  97. data/lib/logster/ignore_pattern.rb +65 -65
  98. data/lib/logster/logger.rb +113 -113
  99. data/lib/logster/message.rb +212 -212
  100. data/lib/logster/middleware/debug_exceptions.rb +26 -26
  101. data/lib/logster/middleware/reporter.rb +55 -55
  102. data/lib/logster/middleware/viewer.rb +297 -222
  103. data/lib/logster/pattern.rb +95 -0
  104. data/lib/logster/rails/railtie.rb +63 -63
  105. data/lib/logster/redis_store.rb +584 -566
  106. data/lib/logster/scheduler.rb +54 -54
  107. data/lib/logster/suppression_pattern.rb +17 -0
  108. data/lib/logster/version.rb +3 -3
  109. data/lib/logster/web.rb +14 -14
  110. data/logster.gemspec +35 -35
  111. data/test/examples/test_sidekiq_reporter_example.rb +46 -46
  112. data/test/fake_data/Gemfile +4 -4
  113. data/test/fake_data/generate.rb +10 -10
  114. data/test/logster/middleware/test_reporter.rb +19 -19
  115. data/test/logster/middleware/test_viewer.rb +267 -96
  116. data/test/logster/test_base_store.rb +147 -147
  117. data/test/logster/test_cache.rb +38 -0
  118. data/test/logster/test_defer_logger.rb +34 -34
  119. data/test/logster/test_ignore_pattern.rb +41 -41
  120. data/test/logster/test_logger.rb +100 -86
  121. data/test/logster/test_message.rb +119 -119
  122. data/test/logster/test_pattern.rb +152 -0
  123. data/test/logster/test_redis_rate_limiter.rb +230 -230
  124. data/test/logster/test_redis_store.rb +689 -720
  125. data/test/test_helper.rb +38 -38
  126. data/vendor/assets/javascripts/logster.js.erb +39 -39
  127. metadata +24 -7
@@ -0,0 +1,95 @@
1
+ module Logster
2
+ class Pattern
3
+ class PatternError < StandardError; end
4
+
5
+ def self.set_name
6
+ raise "Please override the `set_name` method and specify and a name for this set"
7
+ end
8
+
9
+ def self.parse_pattern(string)
10
+ return string if Regexp === string
11
+ return unless String === string
12
+ if string[0] == "/"
13
+ return unless string =~ /\/(.+)\/(.*)/
14
+ string = $1
15
+ flag = Regexp::IGNORECASE if $2 && $2.include?("i")
16
+ end
17
+ Regexp.new(string, flag)
18
+ rescue RegexpError
19
+ nil
20
+ end
21
+
22
+ def self.find_all(raw: false, store: Logster.store)
23
+ patterns = store.get_patterns(set_name) || []
24
+ unless raw
25
+ patterns.map! do |p|
26
+ parse_pattern(p)
27
+ end
28
+ end
29
+ patterns.compact!
30
+ patterns
31
+ end
32
+
33
+ def self.find(pattern, store: Logster.store)
34
+ pattern = parse_pattern(pattern).inspect
35
+ return nil unless pattern
36
+ pattern = find_all(raw: true, store: store).find { |p| p == pattern }
37
+ return nil unless pattern
38
+ new(pattern)
39
+ end
40
+
41
+ def self.valid?(pattern)
42
+ return false unless Regexp === pattern
43
+ pattern_size = pattern.inspect.size
44
+ pattern_size > 3 && pattern_size < 500
45
+ end
46
+
47
+ def initialize(pattern, store: Logster.store)
48
+ self.pattern = pattern
49
+ @store = store
50
+ end
51
+
52
+ def valid?
53
+ self.class.valid?(pattern)
54
+ end
55
+
56
+ def to_s
57
+ pattern.inspect
58
+ end
59
+
60
+ def save
61
+ ensure_valid!
62
+ @store.insert_pattern(set_name, self.to_s)
63
+ end
64
+
65
+ def modify(new_pattern)
66
+ new_pattern = self.class.parse_pattern(new_pattern)
67
+ raise PatternError.new unless self.class.valid?(new_pattern)
68
+ destroy
69
+ self.pattern = new_pattern
70
+ save
71
+ end
72
+
73
+ def destroy
74
+ @store.remove_pattern(set_name, self.to_s)
75
+ end
76
+
77
+ def pattern
78
+ @pattern
79
+ end
80
+
81
+ private
82
+
83
+ def pattern=(new_pattern)
84
+ @pattern = self.class.parse_pattern(new_pattern)
85
+ end
86
+
87
+ def set_name
88
+ self.class.set_name
89
+ end
90
+
91
+ def ensure_valid!
92
+ raise PatternError.new("Invalid pattern") unless valid?
93
+ end
94
+ end
95
+ end
@@ -1,63 +1,63 @@
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
- if Rails.env.development?
17
- require 'logster/defer_logger'
18
- logger = Logster::DeferLogger.new(store)
19
- else
20
- logger = Logster::Logger.new(store)
21
- end
22
-
23
- logger.chain(::Rails.logger)
24
- logger.level = ::Rails.logger.level
25
-
26
- Logster.logger = ::Rails.logger = config.logger = logger
27
- end
28
-
29
- def self.initialize!(app)
30
- return unless Logster.config.environments.include?(Rails.env.to_sym)
31
-
32
- if Logster::Logger === Rails.logger
33
- app.middleware.insert_before ActionDispatch::ShowExceptions, Logster::Middleware::Reporter
34
-
35
- if Rails::VERSION::MAJOR == 3
36
- app.middleware.insert_before ActionDispatch::DebugExceptions, Logster::Middleware::DebugExceptions
37
- else
38
- app.middleware.insert_before ActionDispatch::DebugExceptions, Logster::Middleware::DebugExceptions, Rails.application
39
- end
40
-
41
- app.middleware.delete ActionDispatch::DebugExceptions
42
- app.config.colorize_logging = false
43
-
44
- unless Logster.config.application_version
45
- git_version = `cd #{Rails.root} && git rev-parse --short HEAD 2> /dev/null`
46
- if git_version.present?
47
- Logster.config.application_version = git_version.strip
48
- end
49
- end
50
- end
51
- end
52
-
53
- class Railtie < ::Rails::Railtie
54
-
55
- config.before_initialize do
56
- Logster::Rails.set_logger(config)
57
- end
58
-
59
- initializer "logster.configure_rails_initialization" do |app|
60
- Logster::Rails.initialize!(app)
61
- end
62
- end
63
- 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
+ if Rails.env.development?
17
+ require 'logster/defer_logger'
18
+ logger = Logster::DeferLogger.new(store)
19
+ else
20
+ logger = Logster::Logger.new(store)
21
+ end
22
+
23
+ logger.chain(::Rails.logger)
24
+ logger.level = ::Rails.logger.level
25
+
26
+ Logster.logger = ::Rails.logger = config.logger = logger
27
+ end
28
+
29
+ def self.initialize!(app)
30
+ return unless Logster.config.environments.include?(Rails.env.to_sym)
31
+
32
+ if Logster::Logger === Rails.logger
33
+ app.middleware.insert_before ActionDispatch::ShowExceptions, Logster::Middleware::Reporter
34
+
35
+ if Rails::VERSION::MAJOR == 3
36
+ app.middleware.insert_before ActionDispatch::DebugExceptions, Logster::Middleware::DebugExceptions
37
+ else
38
+ app.middleware.insert_before ActionDispatch::DebugExceptions, Logster::Middleware::DebugExceptions, Rails.application
39
+ end
40
+
41
+ app.middleware.delete ActionDispatch::DebugExceptions
42
+ app.config.colorize_logging = false
43
+
44
+ unless Logster.config.application_version
45
+ git_version = `cd #{Rails.root} && git rev-parse --short HEAD 2> /dev/null`
46
+ if git_version.present?
47
+ Logster.config.application_version = git_version.strip
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ class Railtie < ::Rails::Railtie
54
+
55
+ config.before_initialize do
56
+ Logster::Rails.set_logger(config)
57
+ end
58
+
59
+ initializer "logster.configure_rails_initialization" do |app|
60
+ Logster::Rails.initialize!(app)
61
+ end
62
+ end
63
+ end
@@ -1,566 +1,584 @@
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(env_key, msg.key)
148
- @redis.hdel(grouping_key, msg.grouping_key)
149
- @redis.lrem(list_key, -1, msg.key)
150
- end
151
- end
152
-
153
- def replace_and_bump(message, save_env: true)
154
- # TODO make it atomic
155
- exists = @redis.hexists(hash_key, message.key)
156
- return false unless exists
157
-
158
- @redis.multi do
159
- @redis.hset(hash_key, message.key, message.to_json(exclude_env: true))
160
- @redis.hset(env_key, message.key, (message.env || {}).to_json) if save_env
161
- @redis.lrem(list_key, -1, message.key)
162
- @redis.rpush(list_key, message.key)
163
- end
164
-
165
- check_rate_limits(message.severity)
166
-
167
- true
168
- end
169
-
170
- def similar_key(message)
171
- @redis.hget(grouping_key, message.grouping_key)
172
- end
173
-
174
- def count
175
- @redis.llen(list_key)
176
- end
177
-
178
- def solve(message_key)
179
- if (message = get(message_key)) && (keys = message.solved_keys)
180
- # add a time so we can expire it
181
- keys.each do |s_key|
182
- @redis.hset(solved_key, s_key, Time.now.to_f.to_i)
183
- end
184
- end
185
- clear_solved
186
- end
187
-
188
- def latest(opts = {})
189
- limit = opts[:limit] || 50
190
- severity = opts[:severity]
191
- before = opts[:before]
192
- after = opts[:after]
193
- search = opts[:search]
194
-
195
- start, finish = find_location(before, after, limit)
196
-
197
- return [] unless start && finish
198
-
199
- results = []
200
-
201
- direction = after ? 1 : -1
202
-
203
- begin
204
- keys = @redis.lrange(list_key, start, finish) || []
205
- break unless keys && (keys.count > 0)
206
- rows = bulk_get(keys)
207
-
208
- temp = []
209
-
210
- rows.each do |row|
211
- break if before && before == row.key
212
- row = nil if severity && !severity.include?(row.severity)
213
-
214
- row = filter_search(row, search)
215
- temp << row if row
216
- end
217
-
218
- if direction == -1
219
- results = temp + results
220
- else
221
- results += temp
222
- end
223
-
224
- start += limit * direction
225
- finish += limit * direction
226
-
227
- finish = -1 if finish > -1
228
- end while rows.length > 0 && results.length < limit && start < 0
229
-
230
- results
231
- end
232
-
233
- def clear
234
- RedisRateLimiter.clear_all(@redis)
235
- @redis.del(solved_key)
236
- @redis.del(list_key)
237
- keys = @redis.smembers(protected_key) || []
238
- if keys.empty?
239
- @redis.del(hash_key)
240
- @redis.del(env_key)
241
- else
242
- protected = @redis.mapped_hmget(hash_key, *keys)
243
- protected_env = @redis.mapped_hmget(env_key, *keys)
244
- @redis.del(hash_key)
245
- @redis.del(env_key)
246
- @redis.mapped_hmset(hash_key, protected)
247
- @redis.mapped_hmset(env_key, protected_env)
248
-
249
- sorted = protected
250
- .values
251
- .map { |string|
252
- Message.from_json(string) rescue nil
253
- }
254
- .compact
255
- .sort
256
- .map(&:key)
257
-
258
- @redis.pipelined do
259
- sorted.each do |message_key|
260
- @redis.rpush(list_key, message_key)
261
- end
262
- end
263
- end
264
- end
265
-
266
- # Delete everything, included protected messages
267
- # (use in tests)
268
- def clear_all
269
- @redis.del(list_key)
270
- @redis.del(protected_key)
271
- @redis.del(hash_key)
272
- @redis.del(env_key)
273
- @redis.del(grouping_key)
274
- @redis.del(solved_key)
275
- end
276
-
277
- def get(message_key, load_env: true)
278
- json = @redis.hget(hash_key, message_key)
279
- return nil unless json
280
-
281
- message = Message.from_json(json)
282
- if load_env
283
- message.env = get_env(message_key) || {}
284
- end
285
- message
286
- end
287
-
288
- def bulk_get(message_keys)
289
- envs = @redis.hmget(env_key, message_keys)
290
- @redis.hmget(hash_key, message_keys).map!.with_index do |json, ind|
291
- message = Message.from_json(json)
292
- env = envs[ind]
293
- if !message.env || message.env.size == 0
294
- env = env && env.size > 0 ? ::JSON.parse(env) : {}
295
- message.env = env
296
- end
297
- message
298
- end
299
- end
300
-
301
- def get_env(message_key)
302
- json = @redis.hget(env_key, message_key)
303
- return if !json || json.size == 0
304
- JSON.parse(json)
305
- end
306
-
307
- def protect(message_key)
308
- if message = get(message_key)
309
- message.protected = true
310
- update_message(message)
311
- end
312
- end
313
-
314
- def unprotect(message_key)
315
- if message = get(message_key)
316
- message.protected = false
317
- update_message(message)
318
- else
319
- raise "Message already deleted"
320
- end
321
- end
322
-
323
- def solved
324
- @redis.hkeys(solved_key) || []
325
- end
326
-
327
- def register_rate_limit_per_minute(severities, limit, &block)
328
- register_rate_limit(severities, limit, 60, block)
329
- end
330
-
331
- def register_rate_limit_per_hour(severities, limit, &block)
332
- register_rate_limit(severities, limit, 3600, block)
333
- end
334
-
335
- def redis_prefix
336
- return 'default'.freeze if !@redis_prefix
337
- @prefix_is_proc ||= @redis_prefix.respond_to?(:call)
338
- @prefix_is_proc ? @redis_prefix.call : @redis_prefix
339
- end
340
-
341
- def rate_limits
342
- @rate_limits ||= {}
343
- end
344
-
345
- protected
346
-
347
- def clear_solved(count = nil)
348
-
349
- ignores = Set.new(@redis.hkeys(solved_key) || [])
350
-
351
- if ignores.length > 0
352
- start = count ? 0 - count : 0
353
- message_keys = @redis.lrange(list_key, start, -1) || []
354
-
355
- bulk_get(message_keys).each do |message|
356
- unless (ignores & (message.solved_keys || [])).empty?
357
- delete message
358
- end
359
- end
360
- end
361
- end
362
-
363
- def trim
364
- if @redis.llen(list_key) > max_backlog
365
- removed_keys = []
366
- while removed_key = @redis.lpop(list_key)
367
- unless @redis.sismember(protected_key, removed_key)
368
- rmsg = get removed_key
369
- @redis.hdel(hash_key, rmsg.key)
370
- @redis.hdel(env_key, rmsg.key)
371
- @redis.hdel(grouping_key, rmsg.grouping_key)
372
- break
373
- else
374
- removed_keys << removed_key
375
- end
376
- end
377
- removed_keys.reverse.each do |key|
378
- @redis.lpush(list_key, key)
379
- end
380
- end
381
- end
382
-
383
- def update_message(message)
384
- @redis.hset(hash_key, message.key, message.to_json(exclude_env: true))
385
- @redis.hset(env_key, message.key, (message.env || {}).to_json)
386
- if message.protected
387
- @redis.sadd(protected_key, message.key)
388
- else
389
- @redis.srem(protected_key, message.key)
390
- end
391
- end
392
-
393
- def find_message(list, message_key)
394
- limit = 50
395
- start = 0
396
- finish = limit - 1
397
-
398
- found = nil
399
- while found == nil
400
- items = @redis.lrange(list, start, finish)
401
-
402
- break unless items && items.length > 0
403
-
404
- found = items.index(message_key)
405
- break if found
406
-
407
- start += limit
408
- finish += limit
409
- end
410
-
411
- found
412
- end
413
-
414
- def find_location(before, after, limit)
415
- start = -limit
416
- finish = -1
417
-
418
- return [start, finish] unless before || after
419
-
420
- found = nil
421
- find = before || after
422
-
423
- while !found
424
- items = @redis.lrange(list_key, start, finish)
425
-
426
- break unless items && items.length > 0
427
-
428
- found = items.index(find)
429
-
430
- if items.length < limit
431
- found += limit - items.length if found
432
- break
433
- end
434
- break if found
435
- start -= limit
436
- finish -= limit
437
- end
438
-
439
- if found
440
- if before
441
- offset = -(limit - found)
442
- else
443
- offset = found + 1
444
- end
445
-
446
- start += offset
447
- finish += offset
448
-
449
- finish = -1 if finish > -1
450
- return nil if start > -1
451
- end
452
-
453
- [start, finish]
454
- end
455
-
456
- def get_search(search)
457
- exclude = false
458
- if String === search && search[0] == "-"
459
- exclude = true
460
- search = search.sub("-", "")
461
- end
462
- [search, exclude]
463
- end
464
-
465
- def filter_search(row, search)
466
- search, exclude = get_search(search)
467
- return row unless row && search
468
-
469
- if exclude
470
- row if !(row =~ search) && filter_env!(row, search, exclude)
471
- else
472
- row if row =~ search || filter_env!(row, search)
473
- end
474
- end
475
-
476
- def filter_env!(message, search, exclude = false)
477
- if Array === message.env
478
- array_env_matches?(message, search, exclude)
479
- else
480
- if exclude
481
- !env_matches?(message.env, search)
482
- else
483
- env_matches?(message.env, search)
484
- end
485
- end
486
- end
487
-
488
- def env_matches?(env, search)
489
- return false unless env && search
490
-
491
- env.values.any? do |value|
492
- if Hash === value
493
- env_matches?(value, search)
494
- else
495
- case search
496
- when Regexp
497
- value.to_s =~ search
498
- when String
499
- value.to_s =~ Regexp.new(search, Regexp::IGNORECASE)
500
- else
501
- false
502
- end
503
- end
504
- end
505
- end
506
-
507
- def array_env_matches?(message, search, exclude)
508
- matches = message.env.select do |env|
509
- if exclude
510
- !env_matches?(env, search)
511
- else
512
- env_matches?(env, search)
513
- end
514
- end
515
- return false if matches.empty?
516
- message.env = matches
517
- message.count = matches.size
518
- true
519
- end
520
-
521
- def check_rate_limits(severity)
522
- rate_limits_to_check = rate_limits[self.redis_prefix]
523
- return if !rate_limits_to_check
524
- rate_limits_to_check.each { |rate_limit| rate_limit.check(severity) }
525
- end
526
-
527
- def solved_key
528
- @solved_key ||= "__LOGSTER__SOLVED_MAP"
529
- end
530
-
531
- def list_key
532
- @list_key ||= "__LOGSTER__LATEST"
533
- end
534
-
535
- def hash_key
536
- @hash_key ||= "__LOGSTER__MAP"
537
- end
538
-
539
- def env_key
540
- @env_key ||= "__LOGSTER__ENV_MAP"
541
- end
542
-
543
- def protected_key
544
- @saved_key ||= "__LOGSTER__SAVED"
545
- end
546
-
547
- def grouping_key
548
- @grouping_key ||= "__LOGSTER__GMAP"
549
- end
550
-
551
- private
552
-
553
- def register_rate_limit(severities, limit, duration, callback)
554
- severities = [severities] unless severities.is_a?(Array)
555
- redis = (@redis_raw_connection && @redis_prefix) ? @redis_raw_connection : @redis
556
-
557
- rate_limiter = RedisRateLimiter.new(
558
- redis, severities, limit, duration, Proc.new { redis_prefix }, callback
559
- )
560
-
561
- rate_limits[self.redis_prefix] ||= []
562
- rate_limits[self.redis_prefix] << rate_limiter
563
- rate_limiter
564
- end
565
- end
566
- 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(env_key, msg.key)
148
+ @redis.hdel(grouping_key, msg.grouping_key)
149
+ @redis.lrem(list_key, -1, msg.key)
150
+ end
151
+ end
152
+
153
+ def replace_and_bump(message, save_env: true)
154
+ # TODO make it atomic
155
+ exists = @redis.hexists(hash_key, message.key)
156
+ return false unless exists
157
+
158
+ @redis.multi do
159
+ @redis.hset(hash_key, message.key, message.to_json(exclude_env: true))
160
+ @redis.hset(env_key, message.key, (message.env || {}).to_json) if save_env
161
+ @redis.lrem(list_key, -1, message.key)
162
+ @redis.rpush(list_key, message.key)
163
+ end
164
+
165
+ check_rate_limits(message.severity)
166
+
167
+ true
168
+ end
169
+
170
+ def similar_key(message)
171
+ @redis.hget(grouping_key, message.grouping_key)
172
+ end
173
+
174
+ def count
175
+ @redis.llen(list_key)
176
+ end
177
+
178
+ def solve(message_key)
179
+ if (message = get(message_key)) && (keys = message.solved_keys)
180
+ # add a time so we can expire it
181
+ keys.each do |s_key|
182
+ @redis.hset(solved_key, s_key, Time.now.to_f.to_i)
183
+ end
184
+ end
185
+ clear_solved
186
+ end
187
+
188
+ def latest(opts = {})
189
+ limit = opts[:limit] || 50
190
+ severity = opts[:severity]
191
+ before = opts[:before]
192
+ after = opts[:after]
193
+ search = opts[:search]
194
+
195
+ start, finish = find_location(before, after, limit)
196
+
197
+ return [] unless start && finish
198
+
199
+ results = []
200
+
201
+ direction = after ? 1 : -1
202
+
203
+ begin
204
+ keys = @redis.lrange(list_key, start, finish) || []
205
+ break unless keys && (keys.count > 0)
206
+ rows = bulk_get(keys)
207
+
208
+ temp = []
209
+
210
+ rows.each do |row|
211
+ break if before && before == row.key
212
+ row = nil if severity && !severity.include?(row.severity)
213
+
214
+ row = filter_search(row, search)
215
+ temp << row if row
216
+ end
217
+
218
+ if direction == -1
219
+ results = temp + results
220
+ else
221
+ results += temp
222
+ end
223
+
224
+ start += limit * direction
225
+ finish += limit * direction
226
+
227
+ finish = -1 if finish > -1
228
+ end while rows.length > 0 && results.length < limit && start < 0
229
+
230
+ results
231
+ end
232
+
233
+ def clear
234
+ RedisRateLimiter.clear_all(@redis)
235
+ @redis.del(solved_key)
236
+ @redis.del(list_key)
237
+ keys = @redis.smembers(protected_key) || []
238
+ if keys.empty?
239
+ @redis.del(hash_key)
240
+ @redis.del(env_key)
241
+ else
242
+ protected = @redis.mapped_hmget(hash_key, *keys)
243
+ protected_env = @redis.mapped_hmget(env_key, *keys)
244
+ @redis.del(hash_key)
245
+ @redis.del(env_key)
246
+ @redis.mapped_hmset(hash_key, protected)
247
+ @redis.mapped_hmset(env_key, protected_env)
248
+
249
+ sorted = protected
250
+ .values
251
+ .map { |string|
252
+ Message.from_json(string) rescue nil
253
+ }
254
+ .compact
255
+ .sort
256
+ .map(&:key)
257
+
258
+ @redis.pipelined do
259
+ sorted.each do |message_key|
260
+ @redis.rpush(list_key, message_key)
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+ # Delete everything, included protected messages
267
+ # (use in tests)
268
+ def clear_all
269
+ @redis.del(list_key)
270
+ @redis.del(protected_key)
271
+ @redis.del(hash_key)
272
+ @redis.del(env_key)
273
+ @redis.del(grouping_key)
274
+ @redis.del(solved_key)
275
+ Logster::PATTERNS.each do |klass|
276
+ @redis.del(klass.set_name)
277
+ end
278
+ @redis.keys.each do |key|
279
+ @redis.del(key) if key.include?(Logster::RedisRateLimiter::PREFIX)
280
+ end
281
+ end
282
+
283
+ def get(message_key, load_env: true)
284
+ json = @redis.hget(hash_key, message_key)
285
+ return nil unless json
286
+
287
+ message = Message.from_json(json)
288
+ if load_env
289
+ message.env = get_env(message_key) || {}
290
+ end
291
+ message
292
+ end
293
+
294
+ def bulk_get(message_keys)
295
+ envs = @redis.hmget(env_key, message_keys)
296
+ @redis.hmget(hash_key, message_keys).map!.with_index do |json, ind|
297
+ message = Message.from_json(json)
298
+ env = envs[ind]
299
+ if !message.env || message.env.size == 0
300
+ env = env && env.size > 0 ? ::JSON.parse(env) : {}
301
+ message.env = env
302
+ end
303
+ message
304
+ end
305
+ end
306
+
307
+ def get_env(message_key)
308
+ json = @redis.hget(env_key, message_key)
309
+ return if !json || json.size == 0
310
+ JSON.parse(json)
311
+ end
312
+
313
+ def protect(message_key)
314
+ if message = get(message_key)
315
+ message.protected = true
316
+ update_message(message)
317
+ end
318
+ end
319
+
320
+ def unprotect(message_key)
321
+ if message = get(message_key)
322
+ message.protected = false
323
+ update_message(message)
324
+ else
325
+ raise "Message already deleted"
326
+ end
327
+ end
328
+
329
+ def solved
330
+ @redis.hkeys(solved_key) || []
331
+ end
332
+
333
+ def register_rate_limit_per_minute(severities, limit, &block)
334
+ register_rate_limit(severities, limit, 60, block)
335
+ end
336
+
337
+ def register_rate_limit_per_hour(severities, limit, &block)
338
+ register_rate_limit(severities, limit, 3600, block)
339
+ end
340
+
341
+ def redis_prefix
342
+ return 'default'.freeze if !@redis_prefix
343
+ @prefix_is_proc ||= @redis_prefix.respond_to?(:call)
344
+ @prefix_is_proc ? @redis_prefix.call : @redis_prefix
345
+ end
346
+
347
+ def rate_limits
348
+ @rate_limits ||= {}
349
+ end
350
+
351
+ def insert_pattern(set_name, pattern)
352
+ @redis.sadd(set_name, pattern)
353
+ end
354
+
355
+ def remove_pattern(set_name, pattern)
356
+ @redis.srem(set_name, pattern)
357
+ end
358
+
359
+ def get_patterns(set_name)
360
+ @redis.smembers(set_name)
361
+ end
362
+
363
+ protected
364
+
365
+ def clear_solved(count = nil)
366
+
367
+ ignores = Set.new(@redis.hkeys(solved_key) || [])
368
+
369
+ if ignores.length > 0
370
+ start = count ? 0 - count : 0
371
+ message_keys = @redis.lrange(list_key, start, -1) || []
372
+
373
+ bulk_get(message_keys).each do |message|
374
+ unless (ignores & (message.solved_keys || [])).empty?
375
+ delete message
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ def trim
382
+ if @redis.llen(list_key) > max_backlog
383
+ removed_keys = []
384
+ while removed_key = @redis.lpop(list_key)
385
+ unless @redis.sismember(protected_key, removed_key)
386
+ rmsg = get removed_key
387
+ @redis.hdel(hash_key, rmsg.key)
388
+ @redis.hdel(env_key, rmsg.key)
389
+ @redis.hdel(grouping_key, rmsg.grouping_key)
390
+ break
391
+ else
392
+ removed_keys << removed_key
393
+ end
394
+ end
395
+ removed_keys.reverse.each do |key|
396
+ @redis.lpush(list_key, key)
397
+ end
398
+ end
399
+ end
400
+
401
+ def update_message(message)
402
+ @redis.hset(hash_key, message.key, message.to_json(exclude_env: true))
403
+ @redis.hset(env_key, message.key, (message.env || {}).to_json)
404
+ if message.protected
405
+ @redis.sadd(protected_key, message.key)
406
+ else
407
+ @redis.srem(protected_key, message.key)
408
+ end
409
+ end
410
+
411
+ def find_message(list, message_key)
412
+ limit = 50
413
+ start = 0
414
+ finish = limit - 1
415
+
416
+ found = nil
417
+ while found == nil
418
+ items = @redis.lrange(list, start, finish)
419
+
420
+ break unless items && items.length > 0
421
+
422
+ found = items.index(message_key)
423
+ break if found
424
+
425
+ start += limit
426
+ finish += limit
427
+ end
428
+
429
+ found
430
+ end
431
+
432
+ def find_location(before, after, limit)
433
+ start = -limit
434
+ finish = -1
435
+
436
+ return [start, finish] unless before || after
437
+
438
+ found = nil
439
+ find = before || after
440
+
441
+ while !found
442
+ items = @redis.lrange(list_key, start, finish)
443
+
444
+ break unless items && items.length > 0
445
+
446
+ found = items.index(find)
447
+
448
+ if items.length < limit
449
+ found += limit - items.length if found
450
+ break
451
+ end
452
+ break if found
453
+ start -= limit
454
+ finish -= limit
455
+ end
456
+
457
+ if found
458
+ if before
459
+ offset = -(limit - found)
460
+ else
461
+ offset = found + 1
462
+ end
463
+
464
+ start += offset
465
+ finish += offset
466
+
467
+ finish = -1 if finish > -1
468
+ return nil if start > -1
469
+ end
470
+
471
+ [start, finish]
472
+ end
473
+
474
+ def get_search(search)
475
+ exclude = false
476
+ if String === search && search[0] == "-"
477
+ exclude = true
478
+ search = search.sub("-", "")
479
+ end
480
+ [search, exclude]
481
+ end
482
+
483
+ def filter_search(row, search)
484
+ search, exclude = get_search(search)
485
+ return row unless row && search
486
+
487
+ if exclude
488
+ row if !(row =~ search) && filter_env!(row, search, exclude)
489
+ else
490
+ row if row =~ search || filter_env!(row, search)
491
+ end
492
+ end
493
+
494
+ def filter_env!(message, search, exclude = false)
495
+ if Array === message.env
496
+ array_env_matches?(message, search, exclude)
497
+ else
498
+ if exclude
499
+ !env_matches?(message.env, search)
500
+ else
501
+ env_matches?(message.env, search)
502
+ end
503
+ end
504
+ end
505
+
506
+ def env_matches?(env, search)
507
+ return false unless env && search
508
+
509
+ env.values.any? do |value|
510
+ if Hash === value
511
+ env_matches?(value, search)
512
+ else
513
+ case search
514
+ when Regexp
515
+ value.to_s =~ search
516
+ when String
517
+ value.to_s =~ Regexp.new(search, Regexp::IGNORECASE)
518
+ else
519
+ false
520
+ end
521
+ end
522
+ end
523
+ end
524
+
525
+ def array_env_matches?(message, search, exclude)
526
+ matches = message.env.select do |env|
527
+ if exclude
528
+ !env_matches?(env, search)
529
+ else
530
+ env_matches?(env, search)
531
+ end
532
+ end
533
+ return false if matches.empty?
534
+ message.env = matches
535
+ message.count = matches.size
536
+ true
537
+ end
538
+
539
+ def check_rate_limits(severity)
540
+ rate_limits_to_check = rate_limits[self.redis_prefix]
541
+ return if !rate_limits_to_check
542
+ rate_limits_to_check.each { |rate_limit| rate_limit.check(severity) }
543
+ end
544
+
545
+ def solved_key
546
+ @solved_key ||= "__LOGSTER__SOLVED_MAP"
547
+ end
548
+
549
+ def list_key
550
+ @list_key ||= "__LOGSTER__LATEST"
551
+ end
552
+
553
+ def hash_key
554
+ @hash_key ||= "__LOGSTER__MAP"
555
+ end
556
+
557
+ def env_key
558
+ @env_key ||= "__LOGSTER__ENV_MAP"
559
+ end
560
+
561
+ def protected_key
562
+ @saved_key ||= "__LOGSTER__SAVED"
563
+ end
564
+
565
+ def grouping_key
566
+ @grouping_key ||= "__LOGSTER__GMAP"
567
+ end
568
+
569
+ private
570
+
571
+ def register_rate_limit(severities, limit, duration, callback)
572
+ severities = [severities] unless severities.is_a?(Array)
573
+ redis = (@redis_raw_connection && @redis_prefix) ? @redis_raw_connection : @redis
574
+
575
+ rate_limiter = RedisRateLimiter.new(
576
+ redis, severities, limit, duration, Proc.new { redis_prefix }, callback
577
+ )
578
+
579
+ rate_limits[self.redis_prefix] ||= []
580
+ rate_limits[self.redis_prefix] << rate_limiter
581
+ rate_limiter
582
+ end
583
+ end
584
+ end