logster 2.3.1 → 2.3.2

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.
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