logister-ruby 0.1.2 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3ffbd6d90538c5e0cf39deab256aaf1b0c5c15f5899051e67cfedc6657ac4b5
4
- data.tar.gz: af860be23dab9bd73c8d394e9b1b91b6076f62a6a5f67272991afafba75d83a6
3
+ metadata.gz: 2f3f9f67b976e92e535974c8a243b14f011088bca57e5ab3dc0385de6ba4c003
4
+ data.tar.gz: d744d4996b00d27a2e51e54b5b91134f4c0ae049c76270a41c7ef93deec04189
5
5
  SHA512:
6
- metadata.gz: 59875a74fdee52e22b697b80e4840ba070db495eb905b47d6522576f2e745d4f64081bf0fbf5f8e7cfc48917466c1a8724a389fe36f728b29080ff71dcb79165
7
- data.tar.gz: fb8e6d1898621e57f2fdb9c66a3e08b655151f3f06ed830e5e2a8c96313c4b1e181c1281846a2a62ada0d7332d8a9631630bb5b9abe9bf7a4fc4f14e04a781a3
6
+ metadata.gz: 9a8dbbbdde7778369654c287f9076a89efaadf5a3d03bdd5cb064546bac2b2b72bd53d641b424d3b4bc3cefecfce0bfd1b0a9c50714a8c58506b21c6765b8902
7
+ data.tar.gz: 823a8c724d3aee752558e9936c23f924c774157c83a7b997885ba19a8dbf6aa835277506101f2eea341dde05923353f58127f6b94c0d8cbb6711823c5f43c946
@@ -1,3 +1,5 @@
1
+ require 'socket'
2
+
1
3
  module Logister
2
4
  class Middleware
3
5
  def initialize(app)
@@ -10,12 +12,63 @@ module Logister
10
12
  Logister.report_error(
11
13
  e,
12
14
  context: {
13
- request_id: env['action_dispatch.request_id'],
14
- path: env['PATH_INFO'],
15
- method: env['REQUEST_METHOD']
15
+ request: build_request_context(env),
16
+ app: build_app_context
16
17
  }
17
18
  )
18
19
  raise
19
20
  end
21
+
22
+ private
23
+
24
+ def build_request_context(env)
25
+ ctx = {
26
+ id: env['action_dispatch.request_id'],
27
+ path: env['PATH_INFO'],
28
+ method: env['REQUEST_METHOD'],
29
+ ip: remote_ip(env),
30
+ user_agent: env['HTTP_USER_AGENT']
31
+ }
32
+
33
+ # Params — available if ActionDispatch has already parsed them
34
+ if (params = env['action_dispatch.request.parameters'])
35
+ ctx[:params] = filter_params(params)
36
+ end
37
+
38
+ ctx.compact
39
+ end
40
+
41
+ def build_app_context
42
+ ctx = {
43
+ ruby: RUBY_VERSION,
44
+ hostname: hostname
45
+ }
46
+ ctx[:rails] = Rails::VERSION::STRING if defined?(Rails::VERSION)
47
+ ctx
48
+ end
49
+
50
+ # Respect X-Forwarded-For set by proxies, fall back to REMOTE_ADDR
51
+ def remote_ip(env)
52
+ forwarded = env['HTTP_X_FORWARDED_FOR'].to_s.split(',').first&.strip
53
+ forwarded.nil? || forwarded.empty? ? env['REMOTE_ADDR'] : forwarded
54
+ end
55
+
56
+ # Remove sensitive parameter values the same way Rails does
57
+ SENSITIVE_PARAMS = %w[password password_confirmation token secret api_key
58
+ credit_card cvv ssn].freeze
59
+
60
+ def filter_params(params)
61
+ params.each_with_object({}) do |(k, v), h|
62
+ h[k] = SENSITIVE_PARAMS.any? { |s| k.to_s.downcase.include?(s) } ? '[FILTERED]' : v
63
+ end
64
+ rescue StandardError
65
+ {}
66
+ end
67
+
68
+ def hostname
69
+ Socket.gethostname
70
+ rescue StandardError
71
+ 'unknown'
72
+ end
20
73
  end
21
74
  end
@@ -14,12 +14,15 @@ module Logister
14
14
  return false if ignored_exception?(exception)
15
15
  return false if ignored_path?(context)
16
16
 
17
+ merged_context = context.dup
18
+ merged_context[:user] = current_user_context if current_user_context
19
+
17
20
  payload = build_payload(
18
21
  event_type: 'error',
19
22
  level: level,
20
23
  message: "#{exception.class}: #{exception.message}",
21
24
  fingerprint: fingerprint || default_fingerprint(exception),
22
- context: context.merge(
25
+ context: merged_context.merge(
23
26
  exception: {
24
27
  class: exception.class.to_s,
25
28
  message: exception.message.to_s,
@@ -53,6 +56,20 @@ module Logister
53
56
  @client.publish(payload)
54
57
  end
55
58
 
59
+ # Store user info for the current thread so it is automatically attached to
60
+ # every error reported during this request.
61
+ #
62
+ # Logister.set_user(id: current_user.id, email: current_user.email, name: current_user.name)
63
+ #
64
+ def set_user(id: nil, email: nil, name: nil, **extra)
65
+ ctx = { id: id, email: email, name: name }.merge(extra).compact
66
+ Thread.current[:logister_user] = ctx.empty? ? nil : ctx
67
+ end
68
+
69
+ def clear_user
70
+ Thread.current[:logister_user] = nil
71
+ end
72
+
56
73
  def flush(timeout: 2)
57
74
  @client.flush(timeout: timeout)
58
75
  end
@@ -63,6 +80,10 @@ module Logister
63
80
 
64
81
  private
65
82
 
83
+ def current_user_context
84
+ Thread.current[:logister_user]
85
+ end
86
+
66
87
  def build_payload(event_type:, level:, message:, fingerprint:, context:)
67
88
  {
68
89
  event_type: event_type,
@@ -118,7 +139,36 @@ module Logister
118
139
  end
119
140
 
120
141
  def default_fingerprint(exception)
121
- Digest::SHA256.hexdigest("#{exception.class}|#{exception.message}")[0, 32]
142
+ # Prefer class + first backtrace location so that errors with dynamic
143
+ # values in their message (e.g. "Couldn't find User with 'id'=42") still
144
+ # group together across different IDs / UUIDs.
145
+ location = Array(exception.backtrace).first.to_s
146
+ .sub(/:in\s+.+$/, '') # strip method name
147
+ .sub(/\A.*\/gems\//, 'gems/') # normalise gem paths
148
+ .sub(/\A#{Regexp.escape(Dir.pwd.to_s)}\//, '') # strip app root
149
+
150
+ if location.empty?
151
+ # No backtrace available — scrub common dynamic tokens from the message
152
+ # before hashing so that e.g. "id=42" and "id=99" hash the same way.
153
+ scrubbed = scrub_dynamic_values(exception.message.to_s)
154
+ Digest::SHA256.hexdigest("#{exception.class}|#{scrubbed}")[0, 32]
155
+ else
156
+ Digest::SHA256.hexdigest("#{exception.class}|#{location}")[0, 32]
157
+ end
158
+ end
159
+
160
+ # Strip values that tend to vary per-occurrence but carry no grouping signal:
161
+ # - numeric IDs: id=42, 'id'=42, id: 42
162
+ # - UUIDs
163
+ # - hex digests (≥8 hex chars)
164
+ # - quoted string values in ActiveRecord-style messages
165
+ def scrub_dynamic_values(message)
166
+ message
167
+ .gsub(/\b(id['"]?\s*[=:]\s*)\d+/i, '\1?')
168
+ .gsub(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i, '?')
169
+ .gsub(/\b[0-9a-f]{8,}\b/, '?')
170
+ .gsub(/'[^']{1,64}'/, '?')
171
+ .gsub(/\d+/, '?')
122
172
  end
123
173
  end
124
174
  end
@@ -1,3 +1,3 @@
1
1
  module Logister
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/logister.rb CHANGED
@@ -28,6 +28,14 @@ module Logister
28
28
  reporter.report_metric(**kwargs)
29
29
  end
30
30
 
31
+ def set_user(id: nil, email: nil, name: nil, **extra)
32
+ reporter.set_user(id: id, email: email, name: name, **extra)
33
+ end
34
+
35
+ def clear_user
36
+ reporter.clear_user
37
+ end
38
+
31
39
  def flush(timeout: 2)
32
40
  reporter.flush(timeout: timeout)
33
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logister-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Logister