logster 2.3.1 → 2.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +9 -0
- data/lib/logster.rb +2 -0
- data/lib/logster/base_store.rb +6 -0
- data/lib/logster/configuration.rb +15 -2
- data/lib/logster/middleware/debug_exceptions.rb +9 -6
- data/lib/logster/middleware/reporter.rb +19 -3
- data/lib/logster/rails/railtie.rb +5 -1
- data/lib/logster/redis_rate_limiter.rb +110 -0
- data/lib/logster/redis_store.rb +20 -107
- data/lib/logster/version.rb +3 -1
- data/test/logster/middleware/test_reporter.rb +41 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2f37eb7a03d711e0162719afed21c5d2b085f641764a9cec168eba16761d0ff
|
4
|
+
data.tar.gz: 1398a1d9baea28cdb3015b863075d0d242ab423f11b78db3b7fd074df9de9d03
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 893b198161a8c729c8e8290de1e82568f1b0caa2facb06e149216b456cdf8ce9ed76ea489296f632e431985ce769cbd3bb4c9fe90acb8c1641a2a48522d01c91
|
7
|
+
data.tar.gz: b7a1460c5d42d07bffe1df6fab527fd4ee6a733e10364f212fd8ee19e4180893b03e2b3b669ba9e7ef2efb01cbb91857ce4162e37f17b5a3efa18428a9595d48
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -35,6 +35,15 @@ To run logster in other environments, in `config/application.rb`
|
|
35
35
|
Logster.set_environments([:development, :staging, :production])
|
36
36
|
```
|
37
37
|
|
38
|
+
### Configuration
|
39
|
+
|
40
|
+
Logster can be configured using `Logster.config`:
|
41
|
+
|
42
|
+
- `Logster.config.application_version`: set to a unique identifier denoting version of your app. The "solve" function takes this version into account when suppressing errors.
|
43
|
+
- `Logster.config.enable_js_error_reporting` : enable js error reporting from clients
|
44
|
+
- `Logster.config.rate_limit_error_reporting` : controls automatic 1 minute rate limiting for JS error reporting.
|
45
|
+
- `Logster.config.web_title` : `<title>` tag for logster error page.
|
46
|
+
|
38
47
|
### Tracking Error Rate
|
39
48
|
Logster allows you to register a callback when the rate of errors has exceeded
|
40
49
|
a given limit.
|
data/lib/logster.rb
CHANGED
data/lib/logster/base_store.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Logster
|
2
4
|
class BaseStore
|
3
5
|
|
@@ -115,6 +117,10 @@ module Logster
|
|
115
117
|
{}
|
116
118
|
end
|
117
119
|
|
120
|
+
def rate_limited?(ip_address, perform: false, limit: 60)
|
121
|
+
not_implemented
|
122
|
+
end
|
123
|
+
|
118
124
|
def report(severity, progname, msg, opts = {})
|
119
125
|
return if (!msg || (String === msg && msg.empty?)) && skip_empty
|
120
126
|
return if level && severity < level
|
@@ -1,7 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Logster
|
2
4
|
class Configuration
|
3
|
-
attr_accessor
|
4
|
-
:
|
5
|
+
attr_accessor(
|
6
|
+
:allow_grouping,
|
7
|
+
:application_version,
|
8
|
+
:current_context,
|
9
|
+
:env_expandable_keys,
|
10
|
+
:enable_custom_patterns_via_ui,
|
11
|
+
:enable_js_error_reporting,
|
12
|
+
:environments,
|
13
|
+
:rate_limit_error_reporting,
|
14
|
+
:web_title
|
15
|
+
)
|
5
16
|
|
6
17
|
attr_writer :subdirectory
|
7
18
|
|
@@ -12,6 +23,8 @@ module Logster
|
|
12
23
|
@subdirectory = nil
|
13
24
|
@env_expandable_keys = []
|
14
25
|
@enable_custom_patterns_via_ui = false
|
26
|
+
@rate_limit_error_reporting = true
|
27
|
+
@enable_js_error_reporting = true
|
15
28
|
|
16
29
|
@allow_grouping = false
|
17
30
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Logster::Middleware::DebugExceptions < ActionDispatch::DebugExceptions
|
2
4
|
private
|
3
5
|
|
@@ -13,13 +15,14 @@ class Logster::Middleware::DebugExceptions < ActionDispatch::DebugExceptions
|
|
13
15
|
|
14
16
|
Logster.config.current_context.call(env) do
|
15
17
|
location = exception.backtrace[0]
|
16
|
-
exception_string = exception.to_s
|
17
18
|
|
18
|
-
Logster.logger.add_with_opts(
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
Logster.logger.add_with_opts(
|
20
|
+
::Logger::Severity::FATAL,
|
21
|
+
"#{exception.class} (#{exception})\n#{location}",
|
22
|
+
"web-exception",
|
23
|
+
backtrace: exception.backtrace.join("\n"),
|
24
|
+
env: env
|
25
|
+
)
|
23
26
|
end
|
24
27
|
|
25
28
|
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Logster
|
2
4
|
module Middleware
|
3
5
|
class Reporter
|
4
6
|
|
5
|
-
PATH_INFO = "PATH_INFO"
|
6
|
-
SCRIPT_NAME = "SCRIPT_NAME"
|
7
|
+
PATH_INFO = "PATH_INFO"
|
8
|
+
SCRIPT_NAME = "SCRIPT_NAME"
|
7
9
|
|
8
10
|
def initialize(app, config = {})
|
9
11
|
@app = app
|
@@ -21,7 +23,18 @@ module Logster
|
|
21
23
|
end
|
22
24
|
|
23
25
|
if path == @error_path
|
26
|
+
|
27
|
+
if !Logster.config.enable_js_error_reporting
|
28
|
+
return [403, {}, "Access Denied"]
|
29
|
+
end
|
30
|
+
|
24
31
|
Logster.config.current_context.call(env) do
|
32
|
+
if Logster.config.rate_limit_error_reporting
|
33
|
+
req = Rack::Request.new(env)
|
34
|
+
if Logster.store.rate_limited?(req.ip, perform: true)
|
35
|
+
return [429, {}, "Rate Limited"]
|
36
|
+
end
|
37
|
+
end
|
25
38
|
report_js_error(env)
|
26
39
|
end
|
27
40
|
return [200, {}, ["OK"]]
|
@@ -34,9 +47,10 @@ module Logster
|
|
34
47
|
|
35
48
|
def report_js_error(env)
|
36
49
|
req = Rack::Request.new(env)
|
50
|
+
|
37
51
|
params = req.params
|
38
52
|
|
39
|
-
message = params["message"] || ""
|
53
|
+
message = (params["message"] || "").dup
|
40
54
|
message << "\nUrl: " << params["url"] if params["url"]
|
41
55
|
message << "\nLine: " << params["line"] if params["line"]
|
42
56
|
message << "\nColumn: " << params["column"] if params["column"]
|
@@ -48,6 +62,8 @@ module Logster
|
|
48
62
|
message,
|
49
63
|
backtrace: backtrace,
|
50
64
|
env: env)
|
65
|
+
|
66
|
+
true
|
51
67
|
end
|
52
68
|
|
53
69
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Logster::Rails
|
2
4
|
|
3
5
|
# this magically registers logster.js in the asset pipeline
|
@@ -30,7 +32,9 @@ module Logster::Rails
|
|
30
32
|
return unless Logster.config.environments.include?(Rails.env.to_sym)
|
31
33
|
|
32
34
|
if Logster::Logger === Rails.logger
|
33
|
-
|
35
|
+
if Logster.config.enable_js_error_reporting
|
36
|
+
app.middleware.insert_before ActionDispatch::ShowExceptions, Logster::Middleware::Reporter
|
37
|
+
end
|
34
38
|
|
35
39
|
if Rails::VERSION::MAJOR == 3
|
36
40
|
app.middleware.insert_before ActionDispatch::DebugExceptions, Logster::Middleware::DebugExceptions
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Logster
|
4
|
+
class RedisRateLimiter
|
5
|
+
BUCKETS = 6
|
6
|
+
PREFIX = "__LOGSTER__RATE_LIMIT".freeze
|
7
|
+
|
8
|
+
attr_reader :duration, :callback
|
9
|
+
|
10
|
+
def self.clear_all(redis, redis_prefix = nil)
|
11
|
+
prefix = key_prefix(redis_prefix)
|
12
|
+
|
13
|
+
redis.eval "
|
14
|
+
local keys = redis.call('keys', '*#{prefix}*')
|
15
|
+
if (table.getn(keys) > 0) then
|
16
|
+
redis.call('del', unpack(keys))
|
17
|
+
end
|
18
|
+
"
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(redis, severities, limit, duration, redis_prefix = nil, callback = nil)
|
22
|
+
@severities = severities
|
23
|
+
@limit = limit
|
24
|
+
@duration = duration
|
25
|
+
@callback = callback
|
26
|
+
@redis_prefix = redis_prefix
|
27
|
+
@redis = redis
|
28
|
+
@bucket_range = @duration / BUCKETS
|
29
|
+
@mget_keys = (0..(BUCKETS - 1)).map { |i| "#{key}:#{i}" }
|
30
|
+
end
|
31
|
+
|
32
|
+
def retrieve_rate
|
33
|
+
@redis.mget(@mget_keys).reduce(0) { |sum, value| sum + value.to_i }
|
34
|
+
end
|
35
|
+
|
36
|
+
def check(severity)
|
37
|
+
return unless @severities.include?(severity)
|
38
|
+
time = Time.now.to_i
|
39
|
+
num = bucket_number(time)
|
40
|
+
redis_key = "#{key}:#{num}"
|
41
|
+
|
42
|
+
current_rate = @redis.eval <<-LUA
|
43
|
+
local bucket_number = #{num}
|
44
|
+
local bucket_count = redis.call("INCR", "#{redis_key}")
|
45
|
+
|
46
|
+
if bucket_count == 1 then
|
47
|
+
redis.call("EXPIRE", "#{redis_key}", "#{bucket_expiry(time)}")
|
48
|
+
redis.call("DEL", "#{callback_key}")
|
49
|
+
end
|
50
|
+
|
51
|
+
local function retrieve_rate ()
|
52
|
+
local sum = 0
|
53
|
+
local values = redis.call("MGET", #{mget_keys(num)})
|
54
|
+
for index, value in ipairs(values) do
|
55
|
+
if value ~= false then sum = sum + value end
|
56
|
+
end
|
57
|
+
return sum
|
58
|
+
end
|
59
|
+
|
60
|
+
return (retrieve_rate() + bucket_count)
|
61
|
+
LUA
|
62
|
+
|
63
|
+
if !@redis.get(callback_key) && (current_rate >= @limit)
|
64
|
+
@callback.call(current_rate) if @callback
|
65
|
+
@redis.set(callback_key, 1)
|
66
|
+
end
|
67
|
+
|
68
|
+
current_rate
|
69
|
+
end
|
70
|
+
|
71
|
+
def key
|
72
|
+
# "_LOGSTER_RATE_LIMIT:012:20:30"
|
73
|
+
# Triggers callback when log levels of :debug, :info and :warn occurs 20 times within 30 secs
|
74
|
+
"#{key_prefix}:#{@severities.join("")}:#{@limit}:#{@duration}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def callback_key
|
78
|
+
"#{key}:callback_triggered"
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def self.key_prefix(redis_prefix)
|
84
|
+
if redis_prefix
|
85
|
+
"#{redis_prefix.call}:#{PREFIX}"
|
86
|
+
else
|
87
|
+
PREFIX
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
def key_prefix
|
93
|
+
self.class.key_prefix(@redis_prefix)
|
94
|
+
end
|
95
|
+
|
96
|
+
def mget_keys(bucket_num)
|
97
|
+
keys = @mget_keys.dup
|
98
|
+
keys.delete_at(bucket_num)
|
99
|
+
keys.map { |key| "'#{key}'" }.join(', ')
|
100
|
+
end
|
101
|
+
|
102
|
+
def bucket_number(time)
|
103
|
+
(time % @duration) / @bucket_range
|
104
|
+
end
|
105
|
+
|
106
|
+
def bucket_expiry(time)
|
107
|
+
@duration - ((time % @duration) % @bucket_range)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/logster/redis_store.rb
CHANGED
@@ -1,114 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'logster/base_store'
|
5
|
+
require 'logster/redis_rate_limiter'
|
3
6
|
|
4
7
|
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
8
|
class RedisStore < BaseStore
|
113
9
|
|
114
10
|
attr_accessor :redis, :max_backlog, :redis_raw_connection
|
@@ -278,6 +174,7 @@ module Logster
|
|
278
174
|
end
|
279
175
|
@redis.keys.each do |key|
|
280
176
|
@redis.del(key) if key.include?(Logster::RedisRateLimiter::PREFIX)
|
177
|
+
@redis.del(key) if key.start_with?(ip_rate_limit_key(""))
|
281
178
|
end
|
282
179
|
end
|
283
180
|
|
@@ -373,6 +270,18 @@ module Logster
|
|
373
270
|
@redis.hgetall(ignored_logs_count_key)
|
374
271
|
end
|
375
272
|
|
273
|
+
def rate_limited?(ip_address, perform: false, limit: 60)
|
274
|
+
key = ip_rate_limit_key(ip_address)
|
275
|
+
|
276
|
+
limited = @redis.exists(key)
|
277
|
+
|
278
|
+
if perform && !limited
|
279
|
+
@redis.setex key, limit, ""
|
280
|
+
end
|
281
|
+
|
282
|
+
limited
|
283
|
+
end
|
284
|
+
|
376
285
|
protected
|
377
286
|
|
378
287
|
def clear_solved(count = nil)
|
@@ -583,6 +492,10 @@ module Logster
|
|
583
492
|
@ignored_logs_count_key ||= "__LOGSTER__IGNORED_LOGS_COUNT_KEY__MAP"
|
584
493
|
end
|
585
494
|
|
495
|
+
def ip_rate_limit_key(ip_address)
|
496
|
+
"__LOGSTER__IP_RATE_LIMIT_#{ip_address}"
|
497
|
+
end
|
498
|
+
|
586
499
|
private
|
587
500
|
|
588
501
|
def register_rate_limit(severities, limit, duration, callback)
|
data/lib/logster/version.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative '../../test_helper'
|
2
4
|
require 'rack'
|
3
5
|
require 'logster/redis_store'
|
@@ -5,15 +7,53 @@ require 'logster/middleware/reporter'
|
|
5
7
|
|
6
8
|
class TestReporter < Minitest::Test
|
7
9
|
|
10
|
+
def setup
|
11
|
+
Logster.store = Logster::RedisStore.new
|
12
|
+
Logster.store.clear_all
|
13
|
+
Logster.config.enable_js_error_reporting = true
|
14
|
+
end
|
15
|
+
|
8
16
|
def test_logs_errors
|
9
|
-
|
17
|
+
reporter = Logster::Middleware::Reporter.new(nil)
|
18
|
+
env = Rack::MockRequest.env_for("/logs/report_js_error?message=hello")
|
19
|
+
status, = reporter.call(env)
|
20
|
+
|
21
|
+
assert_equal(200, status)
|
22
|
+
assert_equal(1, Logster.store.count)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_respects_ban_on_errors
|
26
|
+
Logster.config.enable_js_error_reporting = false
|
10
27
|
|
11
28
|
reporter = Logster::Middleware::Reporter.new(nil)
|
12
29
|
env = Rack::MockRequest.env_for("/logs/report_js_error?message=hello")
|
13
30
|
status, = reporter.call(env)
|
14
31
|
|
32
|
+
assert_equal(403, status)
|
33
|
+
assert_equal(0, Logster.store.count)
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_rate_limiting
|
37
|
+
reporter = Logster::Middleware::Reporter.new(nil)
|
38
|
+
env = Rack::MockRequest.env_for("/logs/report_js_error?message=hello")
|
39
|
+
status, = reporter.call(env)
|
40
|
+
|
15
41
|
assert_equal(200, status)
|
16
42
|
assert_equal(1, Logster.store.count)
|
43
|
+
|
44
|
+
reporter = Logster::Middleware::Reporter.new(nil)
|
45
|
+
env = Rack::MockRequest.env_for("/logs/report_js_error?message=hello2")
|
46
|
+
status, = reporter.call(env)
|
47
|
+
|
48
|
+
assert_equal(429, status)
|
49
|
+
assert_equal(1, Logster.store.count)
|
50
|
+
|
51
|
+
reporter = Logster::Middleware::Reporter.new(nil)
|
52
|
+
env = Rack::MockRequest.env_for("/logs/report_js_error?message=hello2", "REMOTE_ADDR" => "100.1.1.2")
|
53
|
+
status, = reporter.call(env)
|
54
|
+
|
55
|
+
assert_equal(200, status)
|
56
|
+
assert_equal(2, Logster.store.count)
|
17
57
|
end
|
18
58
|
|
19
59
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.3.
|
4
|
+
version: 2.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- UI for viewing logs in Rack
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-08-
|
11
|
+
date: 2019-08-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -248,6 +248,7 @@ files:
|
|
248
248
|
- lib/logster/middleware/viewer.rb
|
249
249
|
- lib/logster/pattern.rb
|
250
250
|
- lib/logster/rails/railtie.rb
|
251
|
+
- lib/logster/redis_rate_limiter.rb
|
251
252
|
- lib/logster/redis_store.rb
|
252
253
|
- lib/logster/scheduler.rb
|
253
254
|
- lib/logster/suppression_pattern.rb
|