logster 2.5.1 → 2.6.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +9 -0
  4. data/README.md +15 -1
  5. data/Rakefile +1 -0
  6. data/assets/javascript/client-app.js +204 -168
  7. data/assets/javascript/vendor.js +5132 -5833
  8. data/assets/stylesheets/client-app.css +1 -1
  9. data/client-app/.eslintrc.js +17 -5
  10. data/client-app/.travis.yml +4 -3
  11. data/client-app/app/app.js +5 -7
  12. data/client-app/app/components/actions-menu.js +24 -17
  13. data/client-app/app/components/back-trace.js +148 -0
  14. data/client-app/app/components/env-tab.js +16 -12
  15. data/client-app/app/components/message-info.js +84 -7
  16. data/client-app/app/components/message-row.js +13 -15
  17. data/client-app/app/components/panel-resizer.js +63 -45
  18. data/client-app/app/components/patterns-list.js +6 -6
  19. data/client-app/app/components/update-time.js +13 -13
  20. data/client-app/app/controllers/index.js +4 -2
  21. data/client-app/app/index.html +1 -1
  22. data/client-app/app/initializers/app-init.js +1 -1
  23. data/client-app/app/lib/decorators.js +11 -0
  24. data/client-app/app/lib/preload.js +14 -3
  25. data/client-app/app/lib/utilities.js +63 -36
  26. data/client-app/app/models/group.js +6 -1
  27. data/client-app/app/models/message-collection.js +9 -7
  28. data/client-app/app/models/message.js +25 -20
  29. data/client-app/app/router.js +4 -6
  30. data/client-app/app/styles/app.css +18 -4
  31. data/client-app/app/templates/components/actions-menu.hbs +6 -2
  32. data/client-app/app/templates/components/back-trace.hbs +8 -0
  33. data/client-app/app/templates/components/message-info.hbs +7 -2
  34. data/client-app/app/templates/index.hbs +4 -1
  35. data/client-app/config/environment.js +1 -1
  36. data/client-app/config/optional-features.json +4 -1
  37. data/client-app/ember-cli-build.js +2 -3
  38. data/client-app/package-lock.json +9712 -2884
  39. data/client-app/package.json +25 -22
  40. data/client-app/preload-json-manager.rb +62 -0
  41. data/client-app/testem.js +0 -1
  42. data/client-app/tests/index.html +1 -1
  43. data/client-app/tests/integration/components/back-trace-test.js +109 -0
  44. data/client-app/tests/integration/components/message-info-test.js +4 -3
  45. data/client-app/tests/integration/components/patterns-list-test.js +7 -2
  46. data/lib/logster.rb +1 -0
  47. data/lib/logster/base_store.rb +16 -9
  48. data/lib/logster/configuration.rb +12 -2
  49. data/lib/logster/defer_logger.rb +1 -1
  50. data/lib/logster/logger.rb +12 -0
  51. data/lib/logster/message.rb +89 -30
  52. data/lib/logster/middleware/viewer.rb +44 -8
  53. data/lib/logster/redis_store.rb +69 -51
  54. data/lib/logster/suppression_pattern.rb +1 -1
  55. data/lib/logster/version.rb +1 -1
  56. data/logster.gemspec +1 -1
  57. data/test/logster/middleware/test_viewer.rb +100 -0
  58. data/test/logster/test_base_store.rb +16 -0
  59. data/test/logster/test_defer_logger.rb +1 -1
  60. data/test/logster/test_message.rb +142 -54
  61. data/test/logster/test_redis_store.rb +99 -39
  62. metadata +11 -6
@@ -38,7 +38,7 @@ module Logster
38
38
 
39
39
  elsif resource =~ /\/message\/([0-9a-f]+)$/
40
40
  if env[REQUEST_METHOD] != "DELETE"
41
- return method_not_allowed("DELETE is needed for /clear")
41
+ return method_not_allowed("DELETE")
42
42
  end
43
43
 
44
44
  key = $1
@@ -87,7 +87,7 @@ module Logster
87
87
 
88
88
  elsif resource =~ /\/clear$/
89
89
  if env[REQUEST_METHOD] != "POST"
90
- return method_not_allowed("POST is needed for /clear")
90
+ return method_not_allowed("POST")
91
91
  end
92
92
  Logster.store.clear
93
93
  return [200, {}, ["Messages cleared"]]
@@ -139,12 +139,12 @@ module Logster
139
139
 
140
140
  set_name = $1
141
141
  req = Rack::Request.new(env)
142
- return method_not_allowed if req.request_method == "GET"
142
+ return method_not_allowed(%w[POST PUT DELETE]) if req.request_method == "GET"
143
143
 
144
144
  update_patterns(set_name, req)
145
145
  elsif resource == "/reset-count.json"
146
146
  req = Rack::Request.new(env)
147
- return method_not_allowed("PUT is needed for this endpoint") if req.request_method != "PUT"
147
+ return method_not_allowed("PUT") if req.request_method != "PUT"
148
148
  pattern = nil
149
149
  if [true, "true"].include?(req.params["hard"])
150
150
  pattern = Logster.store.ignore.find do |patt|
@@ -170,6 +170,16 @@ module Logster
170
170
  else
171
171
  not_found
172
172
  end
173
+ elsif resource == '/solve-group'
174
+ return not_allowed unless Logster.config.enable_custom_patterns_via_ui
175
+ req = Rack::Request.new(env)
176
+ return method_not_allowed("POST") if req.request_method != "POST"
177
+ group = Logster.store.find_pattern_groups do |patt|
178
+ patt.inspect == req.params["regex"]
179
+ end.first
180
+ return not_found("No such pattern group exists") if !group
181
+ group.messages_keys.each { |k| Logster.store.solve(k) }
182
+ return [200, {}, []]
173
183
  else
174
184
  not_found
175
185
  end
@@ -243,7 +253,7 @@ module Logster
243
253
  when "DELETE"
244
254
  record.destroy
245
255
  else
246
- return method_not_allowed("Allowed methods: POST, PUT or DELETE")
256
+ return method_not_allowed(%w[POST PUT DELETE])
247
257
  end
248
258
 
249
259
  [200, { "Content-Type" => "application/json" }, [JSON.generate(pattern: record.to_s)]]
@@ -277,8 +287,11 @@ module Logster
277
287
  [403, {}, [message]]
278
288
  end
279
289
 
280
- def method_not_allowed(message = "Method not allowed")
281
- [405, {}, [message]]
290
+ def method_not_allowed(allowed_methods)
291
+ if Array === allowed_methods
292
+ allowed_methods = allowed_methods.join(", ")
293
+ end
294
+ [405, { "Allow" => allowed_methods }, []]
282
295
  end
283
296
 
284
297
  def parse_regex(string)
@@ -317,13 +330,36 @@ module Logster
317
330
  Rack::Utils.escape_html(JSON.fast_generate(payload))
318
331
  end
319
332
 
333
+ def preload_backtrace_data
334
+ gems_data = []
335
+ Gem::Specification.find_all do |gem|
336
+ url = gem.metadata["source_code_uri"] || gem.homepage
337
+ if url && url.match(/^https?:\/\/github.com\//)
338
+ gems_data << { name: gem.name, url: url }
339
+ end
340
+ end
341
+ {
342
+ gems_data: gems_data,
343
+ directories: Logster.config.project_directories
344
+ }
345
+ end
346
+
320
347
  def body(preload)
321
348
  root_url = @logs_path
322
349
  root_url += "/" if root_url[-1] != "/"
323
350
  preload.merge!(
324
351
  env_expandable_keys: Logster.config.env_expandable_keys,
325
- patterns_enabled: Logster.config.enable_custom_patterns_via_ui
352
+ patterns_enabled: Logster.config.enable_custom_patterns_via_ui,
353
+ application_version: Logster.config.application_version
326
354
  )
355
+ backtrace_links_enabled = Logster.config.enable_backtrace_links
356
+ gems_dir = Logster.config.gems_dir
357
+ gems_dir += "/" if gems_dir[-1] != "/"
358
+ preload.merge!(gems_dir: gems_dir, backtrace_links_enabled: backtrace_links_enabled)
359
+
360
+ if backtrace_links_enabled
361
+ preload.merge!(preload_backtrace_data)
362
+ end
327
363
  <<~HTML
328
364
  <!doctype html>
329
365
  <html>
@@ -6,6 +6,7 @@ require 'logster/redis_rate_limiter'
6
6
 
7
7
  module Logster
8
8
  class RedisStore < BaseStore
9
+ ENV_PREFIX = "logster-env-"
9
10
 
10
11
  attr_accessor :redis, :max_backlog, :redis_raw_connection
11
12
  attr_writer :redis_prefix
@@ -21,15 +22,14 @@ module Logster
21
22
  def save(message)
22
23
  if keys = message.solved_keys
23
24
  keys.each do |solved|
24
- return true if @redis.hget(solved_key, solved)
25
+ return false if @redis.hget(solved_key, solved)
25
26
  end
26
27
  end
27
- apply_max_size_limit(message)
28
28
 
29
29
  @redis.multi do
30
30
  @redis.hset(grouping_key, message.grouping_key, message.key)
31
31
  @redis.rpush(list_key, message.key)
32
- update_message(message)
32
+ update_message(message, save_env: true)
33
33
  end
34
34
 
35
35
  trim
@@ -46,7 +46,7 @@ module Logster
46
46
  save_pattern_group(group) if group.changed?
47
47
  end
48
48
  @redis.hdel(hash_key, msg.key)
49
- @redis.hdel(env_key, msg.key)
49
+ delete_env(msg.key)
50
50
  @redis.hdel(grouping_key, msg.grouping_key)
51
51
  @redis.lrem(list_key, -1, msg.key)
52
52
  end
@@ -60,26 +60,26 @@ module Logster
60
60
  save_pattern_group(group) if group.changed?
61
61
  end
62
62
  @redis.hdel(hash_key, message_keys)
63
- @redis.hdel(env_key, message_keys)
64
63
  @redis.hdel(grouping_key, grouping_keys)
65
64
  message_keys.each do |k|
66
65
  @redis.lrem(list_key, -1, k)
66
+ delete_env(k)
67
67
  end
68
68
  end
69
69
  end
70
70
 
71
- def replace_and_bump(message, save_env: true)
71
+ def replace_and_bump(message)
72
72
  # TODO make it atomic
73
73
  exists = @redis.hexists(hash_key, message.key)
74
74
  return false unless exists
75
75
 
76
76
  @redis.multi do
77
77
  @redis.hset(hash_key, message.key, message.to_json(exclude_env: true))
78
- @redis.hset(env_key, message.key, message.env_json) if save_env
78
+ push_env(message.key, message.env_buffer) if message.has_env_buffer?
79
79
  @redis.lrem(list_key, -1, message.key)
80
80
  @redis.rpush(list_key, message.key)
81
81
  end
82
-
82
+ message.env_buffer = [] if message.has_env_buffer?
83
83
  check_rate_limits(message.severity)
84
84
 
85
85
  true
@@ -164,22 +164,21 @@ module Logster
164
164
  def clear
165
165
  RedisRateLimiter.clear_all(@redis)
166
166
  @redis.del(solved_key)
167
+ all_keys = @redis.lrange(list_key, 0, -1)
167
168
  @redis.del(list_key)
168
- keys = @redis.smembers(protected_key) || []
169
- if keys.empty?
169
+ protected_keys = @redis.smembers(protected_key) || []
170
+ if protected_keys.empty?
170
171
  @redis.del(hash_key)
171
- @redis.del(env_key)
172
+ all_keys.each { |k| delete_env(k) }
172
173
  @redis.del(pattern_groups_key)
173
174
  @redis.del(grouping_key)
174
175
  else
175
- protected = @redis.mapped_hmget(hash_key, *keys)
176
- protected_env = @redis.mapped_hmget(env_key, *keys)
176
+ protected_messages = @redis.mapped_hmget(hash_key, *protected_keys)
177
177
  @redis.del(hash_key)
178
- @redis.del(env_key)
179
- @redis.mapped_hmset(hash_key, protected)
180
- @redis.mapped_hmset(env_key, protected_env)
178
+ @redis.mapped_hmset(hash_key, protected_messages)
179
+ (all_keys - protected_keys).each { |k| delete_env(k) }
181
180
 
182
- sorted = protected
181
+ sorted = protected_messages
183
182
  .values
184
183
  .map { |string|
185
184
  Message.from_json(string) rescue nil
@@ -203,10 +202,10 @@ module Logster
203
202
  # Delete everything, included protected messages
204
203
  # (use in tests)
205
204
  def clear_all
205
+ @redis.lrange(list_key, 0, -1).each { |k| delete_env(k) }
206
206
  @redis.del(list_key)
207
207
  @redis.del(protected_key)
208
208
  @redis.del(hash_key)
209
- @redis.del(env_key)
210
209
  @redis.del(grouping_key)
211
210
  @redis.del(solved_key)
212
211
  @redis.del(ignored_logs_count_key)
@@ -235,17 +234,35 @@ module Logster
235
234
  bulk_get(@redis.lrange(list_key, 0, -1), with_env: with_env)
236
235
  end
237
236
 
237
+ BULK_ENV_GET_LUA = <<~LUA
238
+ local results = {};
239
+ for i = 1, table.getn(KEYS), 1 do
240
+ results[i] = { KEYS[i], redis.call('LRANGE', KEYS[i], 0, -1) };
241
+ end
242
+ return results;
243
+ LUA
244
+
238
245
  def bulk_get(message_keys, with_env: true)
239
246
  return [] if !message_keys || message_keys.size == 0
240
- envs = @redis.mapped_hmget(env_key, *message_keys) if with_env
247
+ envs = nil
248
+ if with_env
249
+ envs = {}
250
+ @redis.eval(
251
+ BULK_ENV_GET_LUA,
252
+ keys: message_keys.map { |k| env_prefix(k) }
253
+ ).to_h.each do |k, v|
254
+ next if v.size == 0
255
+ parsed = v.size == 1 ? JSON.parse(v[0]) : v.map { |e| JSON.parse(e) }
256
+ envs[env_unprefix(k)] = parsed
257
+ end
258
+ end
241
259
  messages = @redis.hmget(hash_key, message_keys).map! do |json|
242
260
  next if !json || json.size == 0
243
261
  message = Message.from_json(json)
244
- if with_env
262
+ if with_env && envs
245
263
  env = envs[message.key]
246
264
  if !message.env || message.env.size == 0
247
- env = env && env.size > 0 ? ::JSON.parse(env) : {}
248
- message.env = env
265
+ message.env = env || {}
249
266
  end
250
267
  end
251
268
  message
@@ -255,20 +272,20 @@ module Logster
255
272
  end
256
273
 
257
274
  def get_env(message_key)
258
- json = @redis.hget(env_key, message_key)
259
- return if !json || json.size == 0
260
- JSON.parse(json)
275
+ envs = @redis.lrange(env_prefix(message_key), 0, -1)
276
+ return if !envs || envs.size == 0
277
+ envs.size == 1 ? JSON.parse(envs[0]) : envs.map { |j| JSON.parse(j) }
261
278
  end
262
279
 
263
280
  def protect(message_key)
264
- if message = get(message_key)
281
+ if message = get(message_key, load_env: false)
265
282
  message.protected = true
266
283
  update_message(message)
267
284
  end
268
285
  end
269
286
 
270
287
  def unprotect(message_key)
271
- if message = get(message_key)
288
+ if message = get(message_key, load_env: false)
272
289
  message.protected = false
273
290
  update_message(message)
274
291
  else
@@ -376,13 +393,11 @@ module Logster
376
393
 
377
394
  protected
378
395
 
379
- def clear_solved(count = nil)
380
-
396
+ def clear_solved
381
397
  ignores = Set.new(@redis.hkeys(solved_key) || [])
382
398
 
383
399
  if ignores.length > 0
384
- start = count ? 0 - count : 0
385
- message_keys = @redis.lrange(list_key, start, -1) || []
400
+ message_keys = @redis.lrange(list_key, 0, -1) || []
386
401
 
387
402
  bulk_get(message_keys).each do |message|
388
403
  unless (ignores & (message.solved_keys || [])).empty?
@@ -397,7 +412,7 @@ module Logster
397
412
  removed_keys = []
398
413
  while removed_key = @redis.lpop(list_key)
399
414
  unless @redis.sismember(protected_key, removed_key)
400
- rmsg = get removed_key
415
+ rmsg = get(removed_key, load_env: false)
401
416
  delete(rmsg)
402
417
  break
403
418
  else
@@ -410,9 +425,9 @@ module Logster
410
425
  end
411
426
  end
412
427
 
413
- def update_message(message)
428
+ def update_message(message, save_env: false)
414
429
  @redis.hset(hash_key, message.key, message.to_json(exclude_env: true))
415
- @redis.hset(env_key, message.key, message.env_json)
430
+ push_env(message.key, message.env) if save_env
416
431
  if message.protected
417
432
  @redis.sadd(protected_key, message.key)
418
433
  else
@@ -571,7 +586,7 @@ module Logster
571
586
  end
572
587
 
573
588
  def protected_key
574
- @saved_key ||= "__LOGSTER__SAVED"
589
+ @protected_key ||= "__LOGSTER__SAVED"
575
590
  end
576
591
 
577
592
  def grouping_key
@@ -608,22 +623,6 @@ module Logster
608
623
  )
609
624
  end
610
625
 
611
- def apply_max_size_limit(message)
612
- size = message.to_json(exclude_env: true).bytesize
613
- env_size = message.env_json.bytesize
614
- max_size = Logster.config.maximum_message_size_bytes
615
- if size + env_size > max_size
616
- # env is most likely the reason for the large size
617
- # truncate it so the overall size is < the limit
618
- if Array === message.env
619
- # the - 1 at the end ensures the size goes a little bit below the limit
620
- truncate_at = (message.env.size.to_f * max_size.to_f / (env_size + size)).to_i - 1
621
- truncate_at = 1 if truncate_at < 1
622
- message.env = message.env[0...truncate_at]
623
- end
624
- end
625
- end
626
-
627
626
  def register_rate_limit(severities, limit, duration, callback)
628
627
  severities = [severities] unless severities.is_a?(Array)
629
628
  redis = (@redis_raw_connection && @redis_prefix) ? @redis_raw_connection : @redis
@@ -636,5 +635,24 @@ module Logster
636
635
  rate_limits[self.redis_prefix] << rate_limiter
637
636
  rate_limiter
638
637
  end
638
+
639
+ def push_env(message_key, env)
640
+ prefixed = env_prefix(message_key)
641
+ env = [env] unless Array === env
642
+ @redis.lpush(prefixed, env.map(&:to_json).reverse)
643
+ @redis.ltrim(prefixed, 0, Logster.config.max_env_count_per_message - 1)
644
+ end
645
+
646
+ def delete_env(message_key)
647
+ @redis.del(env_prefix(message_key))
648
+ end
649
+
650
+ def env_unprefix(key)
651
+ key.sub(ENV_PREFIX, "")
652
+ end
653
+
654
+ def env_prefix(key)
655
+ ENV_PREFIX + key
656
+ end
639
657
  end
640
658
  end
@@ -24,7 +24,7 @@ module Logster
24
24
  def retro_delete_messages
25
25
  keys = []
26
26
  grouping_keys = []
27
- @store.get_all_messages.each do |message|
27
+ @store.get_all_messages(with_env: false).each do |message|
28
28
  if message =~ self.pattern
29
29
  keys << message.key
30
30
  grouping_keys << message.grouping_key
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Logster
4
- VERSION = "2.5.1"
4
+ VERSION = "2.6.0"
5
5
  end
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "guard"
33
33
  spec.add_development_dependency "guard-minitest"
34
34
  spec.add_development_dependency "timecop"
35
- spec.add_development_dependency "byebug"
35
+ spec.add_development_dependency "byebug", "~> 11.0.0"
36
36
  spec.add_development_dependency "rubocop", "~> 0.69.0"
37
37
  spec.add_development_dependency "rubocop-discourse"
38
38
  end
@@ -370,4 +370,104 @@ class TestViewer < Minitest::Test
370
370
  response = request.get("/logsie/fetch-env/123456abc.json")
371
371
  assert_equal(404, response.status)
372
372
  end
373
+
374
+ def test_solve_group_api_requires_post_request
375
+ Logster.config.enable_custom_patterns_via_ui = true
376
+ Logster::GroupingPattern.new(/gotta be post/).save
377
+ msg = Logster.store.report(
378
+ Logger::WARN,
379
+ '',
380
+ 'gotta be post 22',
381
+ env: { "application_version" => "abc" },
382
+ backtrace: "aa"
383
+ )
384
+ latest = Logster.store.latest
385
+ assert_equal(1, latest.size)
386
+ assert_equal(msg.key, latest.first["messages"].first.key)
387
+ %i[get head options].each do |m|
388
+ response = request.public_send(m, "/logsie/solve-group", params: { regex: "/gotta be post/" })
389
+ assert_equal(405, response.status)
390
+ assert_equal("POST", response.headers["Allow"])
391
+ end
392
+ latest = Logster.store.latest
393
+ assert_equal(1, latest.size)
394
+ assert_equal(msg.key, latest.first["messages"].first.key)
395
+ ensure
396
+ Logster.config.enable_custom_patterns_via_ui = false
397
+ end
398
+
399
+ def test_solve_group_returns_404_when_pattern_doesnt_exist
400
+ Logster.config.enable_custom_patterns_via_ui = true
401
+ Logster::GroupingPattern.new(/some pattern/).save
402
+ msg = Logster.store.report(
403
+ Logger::WARN,
404
+ '',
405
+ 'some pattern 22',
406
+ env: { "application_version" => "abc" },
407
+ backtrace: "aa"
408
+ )
409
+ latest = Logster.store.latest
410
+ assert_equal(1, latest.size)
411
+ assert_equal(msg.key, latest.first["messages"].first.key)
412
+ response = request.post("/logsie/solve-group", params: { regex: "/i dont exist/" })
413
+ assert_equal(404, response.status)
414
+ latest = Logster.store.latest
415
+ assert_equal(1, latest.size)
416
+ assert_equal(msg.key, latest.first["messages"].first.key)
417
+ ensure
418
+ Logster.config.enable_custom_patterns_via_ui = false
419
+ end
420
+
421
+ def test_solving_grouped_messages
422
+ Logster.config.enable_custom_patterns_via_ui = true
423
+ backtrace = "a b c d"
424
+ Logster::GroupingPattern.new(/test pattern/).save
425
+ msg1 = Logster.store.report(Logger::WARN, '', 'test pattern 1', backtrace: backtrace)
426
+ msg2 = Logster.store.report(
427
+ Logger::WARN,
428
+ '',
429
+ 'test pattern 2',
430
+ env: { "application_version" => "abc" },
431
+ backtrace: backtrace
432
+ )
433
+ msg3 = Logster.store.report(
434
+ Logger::WARN,
435
+ '',
436
+ 'test pattern 3',
437
+ env: [{ "application_version" => "def" }, { "application_version" => "ghi" }],
438
+ backtrace: backtrace
439
+ )
440
+ group = Logster.store.find_pattern_groups { |p| p == /test pattern/ }.first
441
+ assert_equal([msg3, msg2, msg1].map(&:key), group.messages_keys)
442
+
443
+ latest = Logster.store.latest
444
+ assert_equal(1, latest.size)
445
+ assert_equal([msg1, msg2, msg3].map(&:key).sort, latest.first["messages"].map(&:key).sort)
446
+
447
+ response = request.post("/logsie/solve-group", params: { regex: "/test pattern/" })
448
+ group = Logster.store.find_pattern_groups { |p| p == /test pattern/ }.first
449
+ assert_equal([msg1.key], group.messages_keys)
450
+ assert_equal(200, response.status)
451
+
452
+ latest = Logster.store.latest
453
+ # msg1 remains cause it doesn't have application_version
454
+ assert_equal([msg1.key], latest.first["messages"].map(&:key))
455
+ assert_equal(1, latest.size)
456
+
457
+ msg4 = Logster.store.report(Logger::WARN, '', 'test pattern 4', backtrace: backtrace)
458
+ %w[abc def ghi].each do |version|
459
+ Logster.store.report(
460
+ Logger::WARN,
461
+ '',
462
+ 'test pattern 5',
463
+ env: { "application_version" => version },
464
+ backtrace: backtrace
465
+ )
466
+ end
467
+ latest = Logster.store.latest
468
+ assert_equal([msg1.key, msg4.key].sort, latest.first["messages"].map(&:key).sort)
469
+ assert_equal(1, latest.size)
470
+ ensure
471
+ Logster.config.enable_custom_patterns_via_ui = false
472
+ end
373
473
  end