findbug 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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +375 -0
- data/Rakefile +12 -0
- data/app/controllers/findbug/application_controller.rb +105 -0
- data/app/controllers/findbug/dashboard_controller.rb +93 -0
- data/app/controllers/findbug/errors_controller.rb +129 -0
- data/app/controllers/findbug/performance_controller.rb +80 -0
- data/app/jobs/findbug/alert_job.rb +40 -0
- data/app/jobs/findbug/cleanup_job.rb +132 -0
- data/app/jobs/findbug/persist_job.rb +158 -0
- data/app/models/findbug/error_event.rb +197 -0
- data/app/models/findbug/performance_event.rb +237 -0
- data/app/views/findbug/dashboard/index.html.erb +199 -0
- data/app/views/findbug/errors/index.html.erb +137 -0
- data/app/views/findbug/errors/show.html.erb +185 -0
- data/app/views/findbug/performance/index.html.erb +168 -0
- data/app/views/findbug/performance/show.html.erb +203 -0
- data/app/views/layouts/findbug/application.html.erb +601 -0
- data/lib/findbug/alerts/channels/base.rb +75 -0
- data/lib/findbug/alerts/channels/discord.rb +155 -0
- data/lib/findbug/alerts/channels/email.rb +179 -0
- data/lib/findbug/alerts/channels/slack.rb +149 -0
- data/lib/findbug/alerts/channels/webhook.rb +143 -0
- data/lib/findbug/alerts/dispatcher.rb +126 -0
- data/lib/findbug/alerts/throttler.rb +110 -0
- data/lib/findbug/background_persister.rb +142 -0
- data/lib/findbug/capture/context.rb +301 -0
- data/lib/findbug/capture/exception_handler.rb +141 -0
- data/lib/findbug/capture/exception_subscriber.rb +228 -0
- data/lib/findbug/capture/message_handler.rb +104 -0
- data/lib/findbug/capture/middleware.rb +247 -0
- data/lib/findbug/configuration.rb +381 -0
- data/lib/findbug/engine.rb +109 -0
- data/lib/findbug/performance/instrumentation.rb +336 -0
- data/lib/findbug/performance/transaction.rb +193 -0
- data/lib/findbug/processing/data_scrubber.rb +163 -0
- data/lib/findbug/rails/controller_methods.rb +152 -0
- data/lib/findbug/railtie.rb +222 -0
- data/lib/findbug/storage/circuit_breaker.rb +223 -0
- data/lib/findbug/storage/connection_pool.rb +134 -0
- data/lib/findbug/storage/redis_buffer.rb +285 -0
- data/lib/findbug/tasks/findbug.rake +167 -0
- data/lib/findbug/version.rb +5 -0
- data/lib/findbug.rb +216 -0
- data/lib/generators/findbug/install_generator.rb +67 -0
- data/lib/generators/findbug/templates/POST_INSTALL +41 -0
- data/lib/generators/findbug/templates/create_findbug_error_events.rb +44 -0
- data/lib/generators/findbug/templates/create_findbug_performance_events.rb +47 -0
- data/lib/generators/findbug/templates/initializer.rb +157 -0
- data/sig/findbug.rbs +4 -0
- metadata +251 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module Findbug
|
|
7
|
+
module Capture
|
|
8
|
+
# ExceptionHandler provides the public API for capturing exceptions.
|
|
9
|
+
#
|
|
10
|
+
# This is used by:
|
|
11
|
+
# - Findbug.capture_exception (public API)
|
|
12
|
+
# - Manual captures in user code
|
|
13
|
+
#
|
|
14
|
+
# It's separate from Middleware/Subscriber because those are automatic.
|
|
15
|
+
# This is for explicit, manual captures.
|
|
16
|
+
#
|
|
17
|
+
# WHEN TO USE MANUAL CAPTURE
|
|
18
|
+
# ==========================
|
|
19
|
+
#
|
|
20
|
+
# 1. Handled exceptions you still want to track:
|
|
21
|
+
#
|
|
22
|
+
# begin
|
|
23
|
+
# external_api.call
|
|
24
|
+
# rescue ExternalAPIError => e
|
|
25
|
+
# Findbug.capture_exception(e)
|
|
26
|
+
# # Handle gracefully...
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# 2. Exceptions in background jobs (if not auto-captured):
|
|
30
|
+
#
|
|
31
|
+
# class HardWorker
|
|
32
|
+
# def perform
|
|
33
|
+
# do_work
|
|
34
|
+
# rescue => e
|
|
35
|
+
# Findbug.capture_exception(e)
|
|
36
|
+
# raise # Re-raise for Sidekiq retry
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# 3. Exceptions with extra context:
|
|
41
|
+
#
|
|
42
|
+
# Findbug.capture_exception(e, order_id: order.id, action: "payment")
|
|
43
|
+
#
|
|
44
|
+
class ExceptionHandler
|
|
45
|
+
class << self
|
|
46
|
+
# Capture an exception
|
|
47
|
+
#
|
|
48
|
+
# @param exception [Exception] the exception to capture
|
|
49
|
+
# @param extra_context [Hash] additional context for this error
|
|
50
|
+
#
|
|
51
|
+
def capture(exception, extra_context = {})
|
|
52
|
+
return unless Findbug.enabled?
|
|
53
|
+
return unless should_capture?(exception)
|
|
54
|
+
|
|
55
|
+
event_data = build_event_data(exception, extra_context)
|
|
56
|
+
Storage::RedisBuffer.push_error(event_data)
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
Findbug.logger.error("[Findbug] ExceptionHandler failed: #{e.message}")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def should_capture?(exception)
|
|
64
|
+
Findbug.config.should_capture_exception?(exception)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_event_data(exception, extra_context)
|
|
68
|
+
# Get current context and merge with extra
|
|
69
|
+
context = Context.to_h
|
|
70
|
+
context[:extra] = (context[:extra] || {}).merge(extra_context)
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
exception_class: exception.class.name,
|
|
74
|
+
message: exception.message,
|
|
75
|
+
backtrace: clean_backtrace(exception.backtrace),
|
|
76
|
+
severity: "error",
|
|
77
|
+
handled: true, # Manual captures are "handled"
|
|
78
|
+
source: "manual",
|
|
79
|
+
context: context,
|
|
80
|
+
fingerprint: generate_fingerprint(exception),
|
|
81
|
+
captured_at: Time.now.utc.iso8601(3),
|
|
82
|
+
environment: Findbug.config.environment,
|
|
83
|
+
release: Findbug.config.release,
|
|
84
|
+
server: server_info
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def clean_backtrace(backtrace)
|
|
89
|
+
return [] unless backtrace
|
|
90
|
+
|
|
91
|
+
backtrace.first(50).map do |line|
|
|
92
|
+
if defined?(Rails.root) && Rails.root
|
|
93
|
+
line.sub(Rails.root.to_s + "/", "")
|
|
94
|
+
else
|
|
95
|
+
line
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def generate_fingerprint(exception)
|
|
101
|
+
components = [
|
|
102
|
+
exception.class.name,
|
|
103
|
+
normalize_message(exception.message),
|
|
104
|
+
top_frame(exception.backtrace)
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
Digest::SHA256.hexdigest(components.join("\n"))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def normalize_message(message)
|
|
111
|
+
return "" unless message
|
|
112
|
+
|
|
113
|
+
message
|
|
114
|
+
.gsub(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, "{uuid}")
|
|
115
|
+
.gsub(/\b\d+\.?\d*\b/, "{number}")
|
|
116
|
+
.gsub(/'[^']*'/, "'{string}'")
|
|
117
|
+
.gsub(/"[^"]*"/, '"{string}"')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def top_frame(backtrace)
|
|
121
|
+
return "" unless backtrace&.any?
|
|
122
|
+
|
|
123
|
+
app_line = backtrace.find do |line|
|
|
124
|
+
line.include?("/app/") || line.include?("/lib/")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
(app_line || backtrace.first).to_s
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def server_info
|
|
131
|
+
{
|
|
132
|
+
hostname: Socket.gethostname,
|
|
133
|
+
pid: Process.pid,
|
|
134
|
+
ruby_version: RUBY_VERSION,
|
|
135
|
+
rails_version: (Rails.version if defined?(Rails))
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Findbug
|
|
4
|
+
module Capture
|
|
5
|
+
# ExceptionSubscriber integrates with Rails 7's ErrorReporter.
|
|
6
|
+
#
|
|
7
|
+
# RAILS ERROR REPORTER (Rails 7+)
|
|
8
|
+
# ===============================
|
|
9
|
+
#
|
|
10
|
+
# Rails 7 introduced a centralized error reporting API:
|
|
11
|
+
#
|
|
12
|
+
# Rails.error.handle { risky_operation } # Swallows error, reports it
|
|
13
|
+
# Rails.error.record { risky_operation } # Re-raises, but reports first
|
|
14
|
+
#
|
|
15
|
+
# Third-party gems can subscribe to receive ALL reported errors:
|
|
16
|
+
#
|
|
17
|
+
# Rails.error.subscribe(MySubscriber.new)
|
|
18
|
+
#
|
|
19
|
+
# This is better than just middleware because it catches:
|
|
20
|
+
# - Errors handled gracefully with Rails.error.handle
|
|
21
|
+
# - Background job errors
|
|
22
|
+
# - Errors in non-request contexts
|
|
23
|
+
#
|
|
24
|
+
# HOW IT WORKS
|
|
25
|
+
# ============
|
|
26
|
+
#
|
|
27
|
+
# 1. Rails catches an exception
|
|
28
|
+
# 2. Rails calls Rails.error.report(exception, ...)
|
|
29
|
+
# 3. Rails calls our subscriber's #report method
|
|
30
|
+
# 4. We capture the exception asynchronously
|
|
31
|
+
#
|
|
32
|
+
class ExceptionSubscriber
|
|
33
|
+
# Called by Rails when an error is reported
|
|
34
|
+
#
|
|
35
|
+
# @param error [Exception] the exception that occurred
|
|
36
|
+
# @param handled [Boolean] whether the error was handled
|
|
37
|
+
# @param severity [Symbol] :error, :warning, or :info
|
|
38
|
+
# @param context [Hash] additional context from Rails
|
|
39
|
+
# @param source [String] where the error came from
|
|
40
|
+
#
|
|
41
|
+
def report(error, handled:, severity:, context:, source: nil)
|
|
42
|
+
return unless Findbug.enabled?
|
|
43
|
+
return unless should_capture?(error)
|
|
44
|
+
|
|
45
|
+
# Build event data
|
|
46
|
+
event_data = build_event_data(error, handled, severity, context, source)
|
|
47
|
+
|
|
48
|
+
# Push to Redis buffer (async, non-blocking)
|
|
49
|
+
Storage::RedisBuffer.push_error(event_data)
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
# CRITICAL: Never let Findbug crash your app
|
|
52
|
+
Findbug.logger.error("[Findbug] ExceptionSubscriber failed: #{e.message}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def should_capture?(error)
|
|
58
|
+
Findbug.config.should_capture_exception?(error)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_event_data(error, handled, severity, rails_context, source)
|
|
62
|
+
{
|
|
63
|
+
# Exception details
|
|
64
|
+
exception_class: error.class.name,
|
|
65
|
+
message: error.message,
|
|
66
|
+
backtrace: clean_backtrace(error.backtrace),
|
|
67
|
+
|
|
68
|
+
# Metadata
|
|
69
|
+
severity: map_severity(severity),
|
|
70
|
+
handled: handled,
|
|
71
|
+
source: source,
|
|
72
|
+
|
|
73
|
+
# Context from Findbug
|
|
74
|
+
context: Context.to_h,
|
|
75
|
+
|
|
76
|
+
# Context from Rails
|
|
77
|
+
rails_context: sanitize_context(rails_context),
|
|
78
|
+
|
|
79
|
+
# Fingerprint for grouping
|
|
80
|
+
fingerprint: generate_fingerprint(error),
|
|
81
|
+
|
|
82
|
+
# Timing
|
|
83
|
+
captured_at: Time.now.utc.iso8601(3),
|
|
84
|
+
|
|
85
|
+
# Environment info
|
|
86
|
+
environment: Findbug.config.environment,
|
|
87
|
+
release: Findbug.config.release,
|
|
88
|
+
server: server_info
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Clean up the backtrace
|
|
93
|
+
#
|
|
94
|
+
# WHY CLEAN BACKTRACE?
|
|
95
|
+
# --------------------
|
|
96
|
+
# Raw backtraces include:
|
|
97
|
+
# - Full file paths (privacy concern, also verbose)
|
|
98
|
+
# - Gem internals (not useful for debugging YOUR code)
|
|
99
|
+
# - Framework internals (noisy)
|
|
100
|
+
#
|
|
101
|
+
# We clean it to show only relevant lines.
|
|
102
|
+
#
|
|
103
|
+
def clean_backtrace(backtrace)
|
|
104
|
+
return [] unless backtrace
|
|
105
|
+
|
|
106
|
+
# Limit to reasonable size
|
|
107
|
+
backtrace = backtrace.first(50)
|
|
108
|
+
|
|
109
|
+
backtrace.map do |line|
|
|
110
|
+
# Replace full paths with relative paths
|
|
111
|
+
line.sub(Rails.root.to_s + "/", "") if defined?(Rails.root)
|
|
112
|
+
line
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Map Rails severity to our severity levels
|
|
117
|
+
def map_severity(severity)
|
|
118
|
+
case severity
|
|
119
|
+
when :error then "error"
|
|
120
|
+
when :warning then "warning"
|
|
121
|
+
when :info then "info"
|
|
122
|
+
else "error"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Sanitize context from Rails (may contain non-serializable objects)
|
|
127
|
+
def sanitize_context(context)
|
|
128
|
+
return {} unless context.is_a?(Hash)
|
|
129
|
+
|
|
130
|
+
context.transform_values do |value|
|
|
131
|
+
case value
|
|
132
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
|
133
|
+
value
|
|
134
|
+
when Array
|
|
135
|
+
value.map { |v| sanitize_value(v) }
|
|
136
|
+
when Hash
|
|
137
|
+
sanitize_context(value)
|
|
138
|
+
else
|
|
139
|
+
value.to_s
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
rescue StandardError
|
|
143
|
+
{}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def sanitize_value(value)
|
|
147
|
+
case value
|
|
148
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
|
149
|
+
value
|
|
150
|
+
else
|
|
151
|
+
value.to_s
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Generate a fingerprint for grouping similar errors
|
|
156
|
+
#
|
|
157
|
+
# WHAT IS FINGERPRINTING?
|
|
158
|
+
# -----------------------
|
|
159
|
+
# Multiple occurrences of the "same" error should be grouped together.
|
|
160
|
+
# But what makes two errors "the same"?
|
|
161
|
+
#
|
|
162
|
+
# We use:
|
|
163
|
+
# 1. Exception class name (e.g., "NoMethodError")
|
|
164
|
+
# 2. Exception message (normalized to remove variable parts)
|
|
165
|
+
# 3. Top stack frame (where the error originated)
|
|
166
|
+
#
|
|
167
|
+
# This groups errors by WHERE they happened and WHAT type they are.
|
|
168
|
+
#
|
|
169
|
+
def generate_fingerprint(error)
|
|
170
|
+
components = [
|
|
171
|
+
error.class.name,
|
|
172
|
+
normalize_message(error.message),
|
|
173
|
+
top_frame(error.backtrace)
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
Digest::SHA256.hexdigest(components.join("\n"))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Normalize error message for fingerprinting
|
|
180
|
+
#
|
|
181
|
+
# WHY NORMALIZE?
|
|
182
|
+
# --------------
|
|
183
|
+
# Error messages often contain variable data:
|
|
184
|
+
# "undefined method `foo' for nil:NilClass"
|
|
185
|
+
# "Couldn't find User with ID=123"
|
|
186
|
+
# "Connection timed out after 30.5 seconds"
|
|
187
|
+
#
|
|
188
|
+
# If we used these raw, each user ID would create a new "group".
|
|
189
|
+
# We normalize by replacing:
|
|
190
|
+
# - Numbers with {number}
|
|
191
|
+
# - UUIDs with {uuid}
|
|
192
|
+
# - Quoted strings with {string}
|
|
193
|
+
#
|
|
194
|
+
def normalize_message(message)
|
|
195
|
+
return "" unless message
|
|
196
|
+
|
|
197
|
+
message
|
|
198
|
+
.gsub(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, "{uuid}")
|
|
199
|
+
.gsub(/\b\d+\.?\d*\b/, "{number}")
|
|
200
|
+
.gsub(/'[^']*'/, "'{string}'")
|
|
201
|
+
.gsub(/"[^"]*"/, '"{string}"')
|
|
202
|
+
.gsub(/ID=\d+/, "ID={number}")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Get the top relevant stack frame
|
|
206
|
+
def top_frame(backtrace)
|
|
207
|
+
return "" unless backtrace&.any?
|
|
208
|
+
|
|
209
|
+
# Skip framework internals, find first app line
|
|
210
|
+
app_line = backtrace.find do |line|
|
|
211
|
+
line.include?("/app/") || line.include?("/lib/")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
(app_line || backtrace.first).to_s
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Collect server information
|
|
218
|
+
def server_info
|
|
219
|
+
{
|
|
220
|
+
hostname: Socket.gethostname,
|
|
221
|
+
pid: Process.pid,
|
|
222
|
+
ruby_version: RUBY_VERSION,
|
|
223
|
+
rails_version: (Rails.version if defined?(Rails))
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module Findbug
|
|
7
|
+
module Capture
|
|
8
|
+
# MessageHandler captures non-exception events (messages).
|
|
9
|
+
#
|
|
10
|
+
# WHY CAPTURE MESSAGES?
|
|
11
|
+
# =====================
|
|
12
|
+
#
|
|
13
|
+
# Not every important event is an exception. Sometimes you want to track:
|
|
14
|
+
#
|
|
15
|
+
# - Security events: "User exceeded rate limit"
|
|
16
|
+
# - Business events: "Payment failed validation"
|
|
17
|
+
# - Warnings: "External API response slow"
|
|
18
|
+
# - Debug info: "Cache miss for critical key"
|
|
19
|
+
#
|
|
20
|
+
# These aren't exceptions, but you want to see them in your error dashboard
|
|
21
|
+
# alongside actual errors.
|
|
22
|
+
#
|
|
23
|
+
# USAGE
|
|
24
|
+
# =====
|
|
25
|
+
#
|
|
26
|
+
# Findbug.capture_message("User exceeded rate limit", :warning, user_id: 123)
|
|
27
|
+
# Findbug.capture_message("Payment validation failed", :error, order_id: 456)
|
|
28
|
+
# Findbug.capture_message("Scheduled task completed", :info, duration: 45.2)
|
|
29
|
+
#
|
|
30
|
+
class MessageHandler
|
|
31
|
+
class << self
|
|
32
|
+
# Capture a message
|
|
33
|
+
#
|
|
34
|
+
# @param message [String] the message to capture
|
|
35
|
+
# @param level [Symbol] severity level (:info, :warning, :error)
|
|
36
|
+
# @param extra_context [Hash] additional context
|
|
37
|
+
#
|
|
38
|
+
def capture(message, level = :info, extra_context = {})
|
|
39
|
+
return unless Findbug.enabled?
|
|
40
|
+
|
|
41
|
+
event_data = build_event_data(message, level, extra_context)
|
|
42
|
+
Storage::RedisBuffer.push_error(event_data)
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
Findbug.logger.error("[Findbug] MessageHandler failed: #{e.message}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_event_data(message, level, extra_context)
|
|
50
|
+
context = Context.to_h
|
|
51
|
+
context[:extra] = (context[:extra] || {}).merge(extra_context)
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
# For messages, we use a synthetic "exception class"
|
|
55
|
+
exception_class: "Findbug::Message",
|
|
56
|
+
message: message,
|
|
57
|
+
backtrace: caller_backtrace,
|
|
58
|
+
|
|
59
|
+
severity: level.to_s,
|
|
60
|
+
handled: true,
|
|
61
|
+
source: "message",
|
|
62
|
+
|
|
63
|
+
context: context,
|
|
64
|
+
fingerprint: generate_fingerprint(message, level),
|
|
65
|
+
|
|
66
|
+
captured_at: Time.now.utc.iso8601(3),
|
|
67
|
+
environment: Findbug.config.environment,
|
|
68
|
+
release: Findbug.config.release,
|
|
69
|
+
server: server_info
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get a clean backtrace from the caller
|
|
74
|
+
def caller_backtrace
|
|
75
|
+
# Skip Findbug internals, show where the message was captured
|
|
76
|
+
caller.drop_while { |line| line.include?("/findbug/") }
|
|
77
|
+
.first(20)
|
|
78
|
+
.map do |line|
|
|
79
|
+
if defined?(Rails.root) && Rails.root
|
|
80
|
+
line.sub(Rails.root.to_s + "/", "")
|
|
81
|
+
else
|
|
82
|
+
line
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def generate_fingerprint(message, level)
|
|
88
|
+
# For messages, fingerprint by the literal message + level
|
|
89
|
+
# We don't normalize because messages are intentional, not variable
|
|
90
|
+
Digest::SHA256.hexdigest("#{level}:#{message}")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def server_info
|
|
94
|
+
{
|
|
95
|
+
hostname: Socket.gethostname,
|
|
96
|
+
pid: Process.pid,
|
|
97
|
+
ruby_version: RUBY_VERSION,
|
|
98
|
+
rails_version: (Rails.version if defined?(Rails))
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|