lapsoss 0.1.0 → 0.3.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 +4 -4
- data/README.md +153 -733
- data/lib/lapsoss/adapters/appsignal_adapter.rb +7 -8
- data/lib/lapsoss/adapters/base.rb +0 -3
- data/lib/lapsoss/adapters/bugsnag_adapter.rb +12 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +102 -101
- data/lib/lapsoss/adapters/logger_adapter.rb +7 -7
- data/lib/lapsoss/adapters/rollbar_adapter.rb +93 -54
- data/lib/lapsoss/adapters/sentry_adapter.rb +11 -17
- data/lib/lapsoss/backtrace_frame.rb +35 -214
- data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
- data/lib/lapsoss/backtrace_processor.rb +37 -37
- data/lib/lapsoss/client.rb +2 -6
- data/lib/lapsoss/configuration.rb +25 -22
- data/lib/lapsoss/current.rb +9 -1
- data/lib/lapsoss/event.rb +30 -6
- data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
- data/lib/lapsoss/exclusion_configuration.rb +30 -0
- data/lib/lapsoss/exclusion_filter.rb +156 -0
- data/lib/lapsoss/{exclusions.rb → exclusion_presets.rb} +1 -181
- data/lib/lapsoss/fingerprinter.rb +9 -13
- data/lib/lapsoss/http_client.rb +42 -8
- data/lib/lapsoss/merged_scope.rb +63 -0
- data/lib/lapsoss/middleware/base.rb +15 -0
- data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
- data/lib/lapsoss/middleware/event_enricher.rb +19 -0
- data/lib/lapsoss/middleware/event_transformer.rb +19 -0
- data/lib/lapsoss/middleware/exception_filter.rb +43 -0
- data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
- data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
- data/lib/lapsoss/middleware/release_tracker.rb +117 -0
- data/lib/lapsoss/middleware/sample_filter.rb +23 -0
- data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
- data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
- data/lib/lapsoss/middleware.rb +0 -347
- data/lib/lapsoss/pipeline.rb +1 -73
- data/lib/lapsoss/pipeline_builder.rb +69 -0
- data/lib/lapsoss/rails_error_subscriber.rb +42 -0
- data/lib/lapsoss/rails_middleware.rb +78 -0
- data/lib/lapsoss/railtie.rb +22 -50
- data/lib/lapsoss/registry.rb +34 -20
- data/lib/lapsoss/release_providers.rb +110 -0
- data/lib/lapsoss/release_tracker.rb +112 -207
- data/lib/lapsoss/router.rb +3 -5
- data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
- data/lib/lapsoss/sampling/base.rb +11 -0
- data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
- data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
- data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
- data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
- data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
- data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
- data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
- data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
- data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
- data/lib/lapsoss/sampling.rb +0 -326
- data/lib/lapsoss/scope.rb +17 -57
- data/lib/lapsoss/scrubber.rb +16 -18
- data/lib/lapsoss/user_context.rb +18 -198
- data/lib/lapsoss/user_context_integrations.rb +39 -0
- data/lib/lapsoss/user_context_middleware.rb +50 -0
- data/lib/lapsoss/user_context_provider.rb +93 -0
- data/lib/lapsoss/utils.rb +13 -0
- data/lib/lapsoss/validators.rb +14 -27
- data/lib/lapsoss/version.rb +1 -1
- data/lib/lapsoss.rb +12 -25
- metadata +106 -21
data/lib/lapsoss/middleware.rb
CHANGED
@@ -2,352 +2,5 @@
|
|
2
2
|
|
3
3
|
module Lapsoss
|
4
4
|
module Middleware
|
5
|
-
class Base
|
6
|
-
def initialize(app)
|
7
|
-
@app = app
|
8
|
-
end
|
9
|
-
|
10
|
-
def call(event, hint = {})
|
11
|
-
@app.call(event, hint)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
# Built-in middleware for common functionality
|
16
|
-
class SampleFilter < Base
|
17
|
-
def initialize(app, sample_rate: 1.0, sample_callback: nil)
|
18
|
-
super(app)
|
19
|
-
@sample_rate = sample_rate
|
20
|
-
@sample_callback = sample_callback
|
21
|
-
end
|
22
|
-
|
23
|
-
def call(event, hint = {})
|
24
|
-
# Apply custom sampling logic first
|
25
|
-
if @sample_callback
|
26
|
-
return nil unless @sample_callback.call(event, hint)
|
27
|
-
end
|
28
|
-
|
29
|
-
# Apply rate-based sampling
|
30
|
-
if @sample_rate < 1.0
|
31
|
-
return nil if rand > @sample_rate
|
32
|
-
end
|
33
|
-
|
34
|
-
@app.call(event, hint)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
class ExceptionFilter < Base
|
39
|
-
def initialize(app, excluded_exceptions: [], excluded_patterns: [])
|
40
|
-
super(app)
|
41
|
-
@excluded_exceptions = Array(excluded_exceptions)
|
42
|
-
@excluded_patterns = Array(excluded_patterns)
|
43
|
-
end
|
44
|
-
|
45
|
-
def call(event, hint = {})
|
46
|
-
return nil if should_exclude?(event)
|
47
|
-
@app.call(event, hint)
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
def should_exclude?(event)
|
53
|
-
return false unless event.exception
|
54
|
-
|
55
|
-
exception_class = event.exception.class
|
56
|
-
exception_message = event.exception.message
|
57
|
-
|
58
|
-
# Check exact class matches
|
59
|
-
return true if @excluded_exceptions.any? { |klass| exception_class <= klass }
|
60
|
-
|
61
|
-
# Check pattern matches
|
62
|
-
@excluded_patterns.any? do |pattern|
|
63
|
-
case pattern
|
64
|
-
when Regexp
|
65
|
-
exception_message&.match?(pattern) || exception_class.name.match?(pattern)
|
66
|
-
when String
|
67
|
-
exception_message&.include?(pattern) || exception_class.name.include?(pattern)
|
68
|
-
else
|
69
|
-
false
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
class UserContextEnhancer < Base
|
76
|
-
def initialize(app, user_provider: nil, privacy_mode: false)
|
77
|
-
super(app)
|
78
|
-
@user_provider = user_provider
|
79
|
-
@privacy_mode = privacy_mode
|
80
|
-
end
|
81
|
-
|
82
|
-
def call(event, hint = {})
|
83
|
-
enhance_user_context(event, hint)
|
84
|
-
@app.call(event, hint)
|
85
|
-
end
|
86
|
-
|
87
|
-
private
|
88
|
-
|
89
|
-
def enhance_user_context(event, hint)
|
90
|
-
# Get user from provider if available
|
91
|
-
user_data = @user_provider&.call(event, hint) || {}
|
92
|
-
|
93
|
-
# Merge with existing user context
|
94
|
-
existing_user = event.context[:user] || {}
|
95
|
-
enhanced_user = existing_user.merge(user_data)
|
96
|
-
|
97
|
-
# Apply privacy filtering if enabled
|
98
|
-
if @privacy_mode
|
99
|
-
enhanced_user = apply_privacy_filtering(enhanced_user)
|
100
|
-
end
|
101
|
-
|
102
|
-
event.context[:user] = enhanced_user unless enhanced_user.empty?
|
103
|
-
end
|
104
|
-
|
105
|
-
def apply_privacy_filtering(user_data)
|
106
|
-
# Remove sensitive fields in privacy mode
|
107
|
-
sensitive_fields = [:email, :phone, :address, :ssn, :credit_card]
|
108
|
-
filtered = user_data.dup
|
109
|
-
|
110
|
-
sensitive_fields.each do |field|
|
111
|
-
if filtered.key?(field)
|
112
|
-
filtered[field] = "[FILTERED]"
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
filtered
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
class ReleaseTracker < Base
|
121
|
-
def initialize(app, release_provider: nil)
|
122
|
-
super(app)
|
123
|
-
@release_provider = release_provider
|
124
|
-
end
|
125
|
-
|
126
|
-
def call(event, hint = {})
|
127
|
-
add_release_info(event, hint)
|
128
|
-
@app.call(event, hint)
|
129
|
-
end
|
130
|
-
|
131
|
-
private
|
132
|
-
|
133
|
-
def add_release_info(event, hint)
|
134
|
-
release_info = @release_provider&.call(event, hint) || auto_detect_release
|
135
|
-
|
136
|
-
if release_info
|
137
|
-
event.context[:release] = release_info
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def auto_detect_release
|
142
|
-
release_info = {}
|
143
|
-
|
144
|
-
# Try to detect Git information
|
145
|
-
if git_info = detect_git_info
|
146
|
-
release_info.merge!(git_info)
|
147
|
-
end
|
148
|
-
|
149
|
-
# Try to detect deployment info
|
150
|
-
if deployment_info = detect_deployment_info
|
151
|
-
release_info.merge!(deployment_info)
|
152
|
-
end
|
153
|
-
|
154
|
-
release_info.empty? ? nil : release_info
|
155
|
-
end
|
156
|
-
|
157
|
-
def detect_git_info
|
158
|
-
return nil unless File.exist?(".git")
|
159
|
-
|
160
|
-
begin
|
161
|
-
# Get current commit SHA
|
162
|
-
commit_sha = `git rev-parse HEAD`.strip
|
163
|
-
return nil if commit_sha.empty?
|
164
|
-
|
165
|
-
# Get branch name
|
166
|
-
branch = `git rev-parse --abbrev-ref HEAD`.strip
|
167
|
-
branch = nil if branch.empty? || branch == "HEAD"
|
168
|
-
|
169
|
-
# Get commit timestamp
|
170
|
-
commit_time = `git log -1 --format=%ct`.strip
|
171
|
-
commit_timestamp = commit_time.empty? ? nil : Time.at(commit_time.to_i)
|
172
|
-
|
173
|
-
# Get tag if on a tag
|
174
|
-
tag = `git describe --exact-match --tags HEAD 2>/dev/null`.strip
|
175
|
-
tag = nil if tag.empty?
|
176
|
-
|
177
|
-
{
|
178
|
-
commit_sha: commit_sha,
|
179
|
-
branch: branch,
|
180
|
-
tag: tag,
|
181
|
-
commit_timestamp: commit_timestamp
|
182
|
-
}.compact
|
183
|
-
rescue StandardError
|
184
|
-
nil
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
def detect_deployment_info
|
189
|
-
info = {}
|
190
|
-
|
191
|
-
# Check common deployment environment variables
|
192
|
-
info[:deployment_id] = ENV["DEPLOYMENT_ID"] if ENV["DEPLOYMENT_ID"]
|
193
|
-
info[:build_number] = ENV["BUILD_NUMBER"] if ENV["BUILD_NUMBER"]
|
194
|
-
info[:deployment_time] = parse_deployment_time(ENV["DEPLOYMENT_TIME"]) if ENV["DEPLOYMENT_TIME"]
|
195
|
-
|
196
|
-
# Check Heroku
|
197
|
-
if ENV["HEROKU_APP_NAME"]
|
198
|
-
info[:platform] = "heroku"
|
199
|
-
info[:app_name] = ENV["HEROKU_APP_NAME"]
|
200
|
-
info[:dyno] = ENV["DYNO"]
|
201
|
-
info[:slug_commit] = ENV["HEROKU_SLUG_COMMIT"]
|
202
|
-
end
|
203
|
-
|
204
|
-
# Check AWS
|
205
|
-
if ENV["AWS_EXECUTION_ENV"]
|
206
|
-
info[:platform] = "aws"
|
207
|
-
info[:execution_env] = ENV["AWS_EXECUTION_ENV"]
|
208
|
-
info[:region] = ENV["AWS_REGION"]
|
209
|
-
end
|
210
|
-
|
211
|
-
# Check Docker
|
212
|
-
if ENV["DOCKER_CONTAINER_ID"] || File.exist?("/.dockerenv")
|
213
|
-
info[:platform] = "docker"
|
214
|
-
info[:container_id] = ENV["DOCKER_CONTAINER_ID"]
|
215
|
-
end
|
216
|
-
|
217
|
-
# Check Kubernetes
|
218
|
-
if ENV["KUBERNETES_SERVICE_HOST"]
|
219
|
-
info[:platform] = "kubernetes"
|
220
|
-
info[:namespace] = ENV["KUBERNETES_NAMESPACE"]
|
221
|
-
info[:pod_name] = ENV["HOSTNAME"]
|
222
|
-
end
|
223
|
-
|
224
|
-
info.empty? ? nil : info
|
225
|
-
end
|
226
|
-
|
227
|
-
def parse_deployment_time(time_str)
|
228
|
-
Time.parse(time_str)
|
229
|
-
rescue StandardError
|
230
|
-
nil
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
class EventEnricher < Base
|
235
|
-
def initialize(app, enrichers: [])
|
236
|
-
super(app)
|
237
|
-
@enrichers = enrichers
|
238
|
-
end
|
239
|
-
|
240
|
-
def call(event, hint = {})
|
241
|
-
@enrichers.each do |enricher|
|
242
|
-
enricher.call(event, hint)
|
243
|
-
end
|
244
|
-
@app.call(event, hint)
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
class ConditionalFilter < Base
|
249
|
-
def initialize(app, condition)
|
250
|
-
super(app)
|
251
|
-
@condition = condition
|
252
|
-
end
|
253
|
-
|
254
|
-
def call(event, hint = {})
|
255
|
-
return nil unless @condition.call(event, hint)
|
256
|
-
@app.call(event, hint)
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
class EventTransformer < Base
|
261
|
-
def initialize(app, transformer)
|
262
|
-
super(app)
|
263
|
-
@transformer = transformer
|
264
|
-
end
|
265
|
-
|
266
|
-
def call(event, hint = {})
|
267
|
-
transformed_event = @transformer.call(event, hint)
|
268
|
-
return nil unless transformed_event
|
269
|
-
@app.call(transformed_event, hint)
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
class RateLimiter < Base
|
274
|
-
def initialize(app, max_events: 100, time_window: 60)
|
275
|
-
super(app)
|
276
|
-
@max_events = max_events
|
277
|
-
@time_window = time_window
|
278
|
-
@events = []
|
279
|
-
@mutex = Mutex.new
|
280
|
-
end
|
281
|
-
|
282
|
-
def call(event, hint = {})
|
283
|
-
@mutex.synchronize do
|
284
|
-
now = Time.now
|
285
|
-
# Remove old events outside time window
|
286
|
-
@events.reject! { |timestamp| now - timestamp > @time_window }
|
287
|
-
|
288
|
-
# Check if we're over the limit
|
289
|
-
if @events.length >= @max_events
|
290
|
-
return nil
|
291
|
-
end
|
292
|
-
|
293
|
-
# Add current event
|
294
|
-
@events << now
|
295
|
-
end
|
296
|
-
|
297
|
-
@app.call(event, hint)
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
class MetricsCollector < Base
|
302
|
-
def initialize(app, collector: nil)
|
303
|
-
super(app)
|
304
|
-
@collector = collector
|
305
|
-
@metrics = {
|
306
|
-
events_processed: 0,
|
307
|
-
events_dropped: 0,
|
308
|
-
events_by_type: Hash.new(0),
|
309
|
-
events_by_level: Hash.new(0)
|
310
|
-
}
|
311
|
-
@mutex = Mutex.new
|
312
|
-
end
|
313
|
-
|
314
|
-
def call(event, hint = {})
|
315
|
-
@mutex.synchronize do
|
316
|
-
@metrics[:events_processed] += 1
|
317
|
-
@metrics[:events_by_type][event.type] += 1
|
318
|
-
@metrics[:events_by_level][event.level] += 1
|
319
|
-
end
|
320
|
-
|
321
|
-
result = @app.call(event, hint)
|
322
|
-
|
323
|
-
if result.nil?
|
324
|
-
@mutex.synchronize do
|
325
|
-
@metrics[:events_dropped] += 1
|
326
|
-
end
|
327
|
-
end
|
328
|
-
|
329
|
-
# Send to external collector if provided
|
330
|
-
@collector&.call(@metrics.dup, event, hint)
|
331
|
-
|
332
|
-
result
|
333
|
-
end
|
334
|
-
|
335
|
-
def metrics
|
336
|
-
@mutex.synchronize { @metrics.dup }
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
# Middleware wrapper for sampling strategies
|
341
|
-
class SamplingMiddleware < Base
|
342
|
-
def initialize(app, sampler)
|
343
|
-
super(app)
|
344
|
-
@sampler = sampler
|
345
|
-
end
|
346
|
-
|
347
|
-
def call(event, hint = {})
|
348
|
-
return nil unless @sampler.sample?(event, hint)
|
349
|
-
@app.call(event, hint)
|
350
|
-
end
|
351
|
-
end
|
352
5
|
end
|
353
6
|
end
|
data/lib/lapsoss/pipeline.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "middleware"
|
4
|
-
|
5
3
|
module Lapsoss
|
6
4
|
class Pipeline
|
7
5
|
def initialize
|
@@ -46,7 +44,7 @@ module Lapsoss
|
|
46
44
|
|
47
45
|
def build_chain
|
48
46
|
# The final app just returns the event
|
49
|
-
final_app =
|
47
|
+
final_app = ->(event, _hint) { event }
|
50
48
|
|
51
49
|
# Build middleware chain from right to left
|
52
50
|
@middlewares.reverse.reduce(final_app) do |app, middleware_config|
|
@@ -58,74 +56,4 @@ module Lapsoss
|
|
58
56
|
end
|
59
57
|
end
|
60
58
|
end
|
61
|
-
|
62
|
-
class PipelineBuilder
|
63
|
-
def initialize
|
64
|
-
@pipeline = Pipeline.new
|
65
|
-
end
|
66
|
-
|
67
|
-
def sample(rate: 1.0, &block)
|
68
|
-
@pipeline.use(Middleware::SampleFilter, sample_rate: rate, sample_callback: block)
|
69
|
-
self
|
70
|
-
end
|
71
|
-
|
72
|
-
def exclude_exceptions(*exception_classes, patterns: [])
|
73
|
-
@pipeline.use(Middleware::ExceptionFilter,
|
74
|
-
excluded_exceptions: exception_classes,
|
75
|
-
excluded_patterns: patterns)
|
76
|
-
self
|
77
|
-
end
|
78
|
-
|
79
|
-
def enhance_user_context(provider: nil, privacy_mode: false)
|
80
|
-
@pipeline.use(Middleware::UserContextEnhancer,
|
81
|
-
user_provider: provider,
|
82
|
-
privacy_mode: privacy_mode)
|
83
|
-
self
|
84
|
-
end
|
85
|
-
|
86
|
-
def track_releases(provider: nil)
|
87
|
-
@pipeline.use(Middleware::ReleaseTracker, release_provider: provider)
|
88
|
-
self
|
89
|
-
end
|
90
|
-
|
91
|
-
def rate_limit(max_events: 100, time_window: 60)
|
92
|
-
@pipeline.use(Middleware::RateLimiter,
|
93
|
-
max_events: max_events,
|
94
|
-
time_window: time_window)
|
95
|
-
self
|
96
|
-
end
|
97
|
-
|
98
|
-
def collect_metrics(collector: nil)
|
99
|
-
@pipeline.use(Middleware::MetricsCollector, collector: collector)
|
100
|
-
self
|
101
|
-
end
|
102
|
-
|
103
|
-
def enrich_events(*enrichers)
|
104
|
-
@pipeline.use(Middleware::EventEnricher, enrichers: enrichers)
|
105
|
-
self
|
106
|
-
end
|
107
|
-
|
108
|
-
def filter_if(&condition)
|
109
|
-
@pipeline.use(Middleware::ConditionalFilter, condition)
|
110
|
-
self
|
111
|
-
end
|
112
|
-
|
113
|
-
def transform_events(&transformer)
|
114
|
-
@pipeline.use(Middleware::EventTransformer, transformer)
|
115
|
-
self
|
116
|
-
end
|
117
|
-
|
118
|
-
def use_middleware(middleware_class, *args, **kwargs)
|
119
|
-
@pipeline.use(middleware_class, *args, **kwargs)
|
120
|
-
self
|
121
|
-
end
|
122
|
-
|
123
|
-
def build
|
124
|
-
@pipeline.build
|
125
|
-
end
|
126
|
-
|
127
|
-
def pipeline
|
128
|
-
@pipeline
|
129
|
-
end
|
130
|
-
end
|
131
59
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
class PipelineBuilder
|
5
|
+
def initialize
|
6
|
+
@pipeline = Pipeline.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def sample(rate: 1.0, &block)
|
10
|
+
@pipeline.use(Middleware::SampleFilter, sample_rate: rate, sample_callback: block)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def exclude_exceptions(*exception_classes, patterns: [])
|
15
|
+
@pipeline.use(Middleware::ExceptionFilter,
|
16
|
+
excluded_exceptions: exception_classes,
|
17
|
+
excluded_patterns: patterns)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def enhance_user_context(provider: nil, privacy_mode: false)
|
22
|
+
@pipeline.use(Middleware::UserContextEnhancer,
|
23
|
+
user_provider: provider,
|
24
|
+
privacy_mode: privacy_mode)
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def track_releases(provider: nil)
|
29
|
+
@pipeline.use(Middleware::ReleaseTracker, release_provider: provider)
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def rate_limit(max_events: 100, time_window: 60)
|
34
|
+
@pipeline.use(Middleware::RateLimiter,
|
35
|
+
max_events: max_events,
|
36
|
+
time_window: time_window)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def collect_metrics(collector: nil)
|
41
|
+
@pipeline.use(Middleware::MetricsCollector, collector: collector)
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def enrich_events(*enrichers)
|
46
|
+
@pipeline.use(Middleware::EventEnricher, enrichers: enrichers)
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def filter_if(&condition)
|
51
|
+
@pipeline.use(Middleware::ConditionalFilter, condition)
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def transform_events(&transformer)
|
56
|
+
@pipeline.use(Middleware::EventTransformer, transformer)
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def use_middleware(middleware_class, *, **)
|
61
|
+
@pipeline.use(middleware_class, *, **)
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
delegate :build, to: :@pipeline
|
66
|
+
|
67
|
+
attr_reader :pipeline
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
class RailsErrorSubscriber
|
5
|
+
def report(error, handled:, severity:, context:, source: nil)
|
6
|
+
# Skip certain framework errors
|
7
|
+
return if skip_error?(error, source)
|
8
|
+
|
9
|
+
level = map_severity(severity)
|
10
|
+
|
11
|
+
Lapsoss.capture_exception(
|
12
|
+
error,
|
13
|
+
level: level,
|
14
|
+
tags: {
|
15
|
+
handled: handled,
|
16
|
+
source: source || "rails"
|
17
|
+
},
|
18
|
+
context: context
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def skip_error?(error, source)
|
25
|
+
# Skip cache-related Redis errors if configured to do so
|
26
|
+
if Lapsoss.configuration.skip_rails_cache_errors && source&.include?("cache") && error.is_a?(Redis::CannotConnectError)
|
27
|
+
return true
|
28
|
+
end
|
29
|
+
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def map_severity(severity)
|
34
|
+
case severity
|
35
|
+
when :error then :error
|
36
|
+
when :warning then :warning
|
37
|
+
when :info then :info
|
38
|
+
else :error
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
class RailsMiddleware
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
Lapsoss::Current.with_clean_scope do
|
11
|
+
# Add request context to current scope
|
12
|
+
if Lapsoss.configuration.capture_request_context
|
13
|
+
Rails.logger.debug "[Lapsoss] Adding request context" if Rails.env.test?
|
14
|
+
add_request_context(env)
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
@app.call(env)
|
19
|
+
rescue Exception => e
|
20
|
+
Rails.logger.debug { "[Lapsoss] Capturing exception: #{e.class} - #{e.message}" } if Rails.env.test?
|
21
|
+
# Capture the exception
|
22
|
+
Lapsoss.capture_exception(e)
|
23
|
+
# Re-raise the exception to maintain Rails error handling
|
24
|
+
raise
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def add_request_context(env)
|
32
|
+
request = Rack::Request.new(env)
|
33
|
+
|
34
|
+
return unless Lapsoss::Current.scope
|
35
|
+
|
36
|
+
Lapsoss::Current.scope.set_context("request", {
|
37
|
+
method: request.request_method,
|
38
|
+
url: request.url,
|
39
|
+
path: request.path,
|
40
|
+
query_string: request.query_string,
|
41
|
+
headers: extract_headers(env),
|
42
|
+
ip: request.ip,
|
43
|
+
user_agent: request.user_agent,
|
44
|
+
referer: request.referer,
|
45
|
+
request_id: env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
|
46
|
+
})
|
47
|
+
|
48
|
+
# Add user context if available
|
49
|
+
return unless env["warden"]&.user
|
50
|
+
|
51
|
+
user = env["warden"].user
|
52
|
+
Lapsoss::Current.scope.set_user(
|
53
|
+
id: user.id,
|
54
|
+
email: user.respond_to?(:email) ? user.email : nil
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def extract_headers(env)
|
59
|
+
headers = {}
|
60
|
+
|
61
|
+
env.each do |key, value|
|
62
|
+
if key.start_with?("HTTP_") && FILTERED_HEADERS.exclude?(key)
|
63
|
+
header_name = key.sub(/^HTTP_/, "").split("_").map(&:capitalize).join("-")
|
64
|
+
headers[header_name] = value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
headers
|
69
|
+
end
|
70
|
+
|
71
|
+
FILTERED_HEADERS = %w[
|
72
|
+
HTTP_AUTHORIZATION
|
73
|
+
HTTP_COOKIE
|
74
|
+
HTTP_X_API_KEY
|
75
|
+
HTTP_X_AUTH_TOKEN
|
76
|
+
].freeze
|
77
|
+
end
|
78
|
+
end
|