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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d19b6d2558ca3ac27df16998d9bab79aabc9e98f15d28894490e61b4d0f7987
4
- data.tar.gz: 5bb3053384ea1203e96ddbd26c11cccb88ba66e5fa3a7f342f4ae00049d2a1a8
3
+ metadata.gz: f2f37eb7a03d711e0162719afed21c5d2b085f641764a9cec168eba16761d0ff
4
+ data.tar.gz: 1398a1d9baea28cdb3015b863075d0d242ab423f11b78db3b7fd074df9de9d03
5
5
  SHA512:
6
- metadata.gz: 65f024a72e62ab478c7e77ad02193e9d8d8d88ab90f7b544b4c6751683600e52d776bc0bf56dd7a6f65c85e47bed7be7a28c7967db1011ee0f8b3ec9ef142863
7
- data.tar.gz: b0e4b102c3de5523ec8057fdc26234d60d70958a5fdd683783f1d59e0dc054a145869605f8c76e4dacfc1c8b49ba74b890abe681c66003194d55033cfcebcaea
6
+ metadata.gz: 893b198161a8c729c8e8290de1e82568f1b0caa2facb06e149216b456cdf8ce9ed76ea489296f632e431985ce769cbd3bb4c9fe90acb8c1641a2a48522d01c91
7
+ data.tar.gz: b7a1460c5d42d07bffe1df6fab527fd4ee6a733e10364f212fd8ee19e4180893b03e2b3b669ba9e7ef2efb01cbb91857ce4162e37f17b5a3efa18428a9595d48
@@ -1,5 +1,9 @@
1
1
  # CHANGELOG
2
2
 
3
+ - 2019-08-20: 2.3.2
4
+
5
+ - FEATURE: automatic 1 minute rate limiting for js error reporting per IP
6
+
3
7
  - 2019-08-15: 2.3.1
4
8
 
5
9
  - DEV: upgrade Ember to 3.8 and jQuery to 3.4.1 (#84)
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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logster/logger'
2
4
  require 'logster/message'
3
5
  require 'logster/configuration'
@@ -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 :current_context, :allow_grouping, :environments,
4
- :application_version, :web_title, :env_expandable_keys, :enable_custom_patterns_via_ui
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(::Logger::Severity::FATAL,
19
- exception.class.to_s << " (" << exception_string << ")\n#{location}",
20
- "web-exception",
21
- backtrace: exception.backtrace.join("\n"),
22
- env: env)
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".freeze
6
- SCRIPT_NAME = "SCRIPT_NAME".freeze
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
- app.middleware.insert_before ActionDispatch::ShowExceptions, Logster::Middleware::Reporter
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
@@ -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)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Logster
2
- VERSION = "2.3.1"
4
+ VERSION = "2.3.2"
3
5
  end
@@ -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
- Logster.store = Logster::TestStore.new
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.1
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-15 00:00:00.000000000 Z
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