lapsoss 0.2.0 → 0.3.1

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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -733
  3. data/lib/lapsoss/adapters/appsignal_adapter.rb +22 -22
  4. data/lib/lapsoss/adapters/base.rb +0 -3
  5. data/lib/lapsoss/adapters/insight_hub_adapter.rb +108 -104
  6. data/lib/lapsoss/adapters/logger_adapter.rb +1 -1
  7. data/lib/lapsoss/adapters/rollbar_adapter.rb +108 -68
  8. data/lib/lapsoss/adapters/sentry_adapter.rb +24 -24
  9. data/lib/lapsoss/backtrace_frame.rb +37 -206
  10. data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
  11. data/lib/lapsoss/backtrace_processor.rb +27 -23
  12. data/lib/lapsoss/client.rb +2 -4
  13. data/lib/lapsoss/configuration.rb +28 -32
  14. data/lib/lapsoss/current.rb +10 -2
  15. data/lib/lapsoss/event.rb +28 -5
  16. data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
  17. data/lib/lapsoss/exclusion_configuration.rb +30 -0
  18. data/lib/lapsoss/exclusion_filter.rb +0 -273
  19. data/lib/lapsoss/exclusion_presets.rb +249 -0
  20. data/lib/lapsoss/fingerprinter.rb +28 -28
  21. data/lib/lapsoss/http_client.rb +8 -8
  22. data/lib/lapsoss/merged_scope.rb +63 -0
  23. data/lib/lapsoss/middleware/base.rb +15 -0
  24. data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
  25. data/lib/lapsoss/middleware/event_enricher.rb +19 -0
  26. data/lib/lapsoss/middleware/event_transformer.rb +19 -0
  27. data/lib/lapsoss/middleware/exception_filter.rb +43 -0
  28. data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
  29. data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
  30. data/lib/lapsoss/middleware/release_tracker.rb +117 -0
  31. data/lib/lapsoss/middleware/sample_filter.rb +23 -0
  32. data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
  33. data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
  34. data/lib/lapsoss/pipeline.rb +0 -68
  35. data/lib/lapsoss/pipeline_builder.rb +69 -0
  36. data/lib/lapsoss/rails_error_subscriber.rb +42 -0
  37. data/lib/lapsoss/rails_middleware.rb +78 -0
  38. data/lib/lapsoss/railtie.rb +22 -50
  39. data/lib/lapsoss/registry.rb +18 -5
  40. data/lib/lapsoss/release_providers.rb +110 -0
  41. data/lib/lapsoss/release_tracker.rb +159 -232
  42. data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
  43. data/lib/lapsoss/sampling/base.rb +11 -0
  44. data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
  45. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
  46. data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
  47. data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
  48. data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
  49. data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
  50. data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
  51. data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
  52. data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
  53. data/lib/lapsoss/scope.rb +12 -48
  54. data/lib/lapsoss/scrubber.rb +7 -7
  55. data/lib/lapsoss/user_context.rb +30 -203
  56. data/lib/lapsoss/user_context_integrations.rb +39 -0
  57. data/lib/lapsoss/user_context_middleware.rb +50 -0
  58. data/lib/lapsoss/user_context_provider.rb +93 -0
  59. data/lib/lapsoss/utils.rb +13 -0
  60. data/lib/lapsoss/validators.rb +15 -15
  61. data/lib/lapsoss/version.rb +1 -1
  62. data/lib/lapsoss.rb +3 -3
  63. metadata +60 -7
  64. data/lib/lapsoss/middleware.rb +0 -345
  65. data/lib/lapsoss/sampling.rb +0 -328
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'faraday'
4
- require 'faraday/retry'
5
- require 'json'
6
- require 'zlib'
3
+ require "faraday"
4
+ require "faraday/retry"
5
+ require "json"
6
+ require "zlib"
7
7
 
8
8
  module Lapsoss
9
9
  # HTTP client wrapper using Faraday with retry logic
@@ -56,7 +56,7 @@ module Lapsoss
56
56
  conn.ssl.verify = @config[:ssl_verify] if @config.key?(:ssl_verify)
57
57
 
58
58
  # Set user agent
59
- conn.headers['User-Agent'] = USER_AGENT
59
+ conn.headers["User-Agent"] = USER_AGENT
60
60
 
61
61
  # Auto-detect and use appropriate adapter
62
62
  conn.adapter detect_optimal_adapter
@@ -78,7 +78,7 @@ module Lapsoss
78
78
  end
79
79
 
80
80
  def async_adapter_available?
81
- require 'async/http/faraday'
81
+ require "async/http/faraday"
82
82
  true
83
83
  rescue LoadError
84
84
  false
@@ -105,8 +105,8 @@ module Lapsoss
105
105
  interval: @config[:initial_backoff] || 1.0,
106
106
  max_interval: @config[:max_backoff] || 64.0,
107
107
  backoff_factor: @config[:backoff_multiplier] || 2.0,
108
- retry_statuses: [408, 429, 500, 502, 503, 504],
109
- methods: [:post]
108
+ retry_statuses: [ 408, 429, 500, 502, 503, 504 ],
109
+ methods: [ :post ]
110
110
  }
111
111
  end
112
112
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ # Performance-optimized scope that provides a merged view without cloning
5
+ class MergedScope
6
+ def initialize(scope_stack, base_scope)
7
+ @scope_stack = scope_stack
8
+ @base_scope = base_scope || Scope.new
9
+ @own_breadcrumbs = []
10
+ end
11
+
12
+ def tags
13
+ @tags ||= merge_hash_contexts(:tags)
14
+ end
15
+
16
+ def user
17
+ @user ||= merge_hash_contexts(:user)
18
+ end
19
+
20
+ def extra
21
+ @extra ||= merge_hash_contexts(:extra)
22
+ end
23
+
24
+ def breadcrumbs
25
+ @breadcrumbs ||= merge_breadcrumbs
26
+ end
27
+
28
+ def add_breadcrumb(message, type: :default, **metadata)
29
+ breadcrumb = {
30
+ message: message,
31
+ type: type,
32
+ metadata: metadata,
33
+ timestamp: Time.now.utc
34
+ }
35
+ @own_breadcrumbs << breadcrumb
36
+ # Keep breadcrumbs to a reasonable limit
37
+ @own_breadcrumbs.shift if @own_breadcrumbs.length > 20
38
+ # Clear cached breadcrumbs to force recomputation
39
+ @breadcrumbs = nil
40
+ end
41
+
42
+ private
43
+
44
+ def merge_hash_contexts(key)
45
+ result = @base_scope.send(key).dup
46
+ @scope_stack.each do |context|
47
+ result.merge!(context[key] || {})
48
+ end
49
+ result
50
+ end
51
+
52
+ def merge_breadcrumbs
53
+ result = @base_scope.breadcrumbs.dup
54
+ @scope_stack.each do |context|
55
+ result.concat(context[:breadcrumbs]) if context[:breadcrumbs]
56
+ end
57
+ # Add our own breadcrumbs
58
+ result.concat(@own_breadcrumbs)
59
+ # Keep breadcrumbs to a reasonable limit
60
+ result.last(20)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
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
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Middleware
5
+ class ConditionalFilter < Base
6
+ def initialize(app, condition)
7
+ super(app)
8
+ @condition = condition
9
+ end
10
+
11
+ def call(event, hint = {})
12
+ return nil unless @condition.call(event, hint)
13
+
14
+ @app.call(event, hint)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Middleware
5
+ class EventEnricher < Base
6
+ def initialize(app, enrichers: [])
7
+ super(app)
8
+ @enrichers = enrichers
9
+ end
10
+
11
+ def call(event, hint = {})
12
+ @enrichers.each do |enricher|
13
+ enricher.call(event, hint)
14
+ end
15
+ @app.call(event, hint)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Middleware
5
+ class EventTransformer < Base
6
+ def initialize(app, transformer)
7
+ super(app)
8
+ @transformer = transformer
9
+ end
10
+
11
+ def call(event, hint = {})
12
+ transformed_event = @transformer.call(event, hint)
13
+ return nil unless transformed_event
14
+
15
+ @app.call(transformed_event, hint)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Middleware
5
+ class ExceptionFilter < Base
6
+ def initialize(app, excluded_exceptions: [], excluded_patterns: [])
7
+ super(app)
8
+ @excluded_exceptions = Array(excluded_exceptions)
9
+ @excluded_patterns = Array(excluded_patterns)
10
+ end
11
+
12
+ def call(event, hint = {})
13
+ return nil if should_exclude?(event)
14
+
15
+ @app.call(event, hint)
16
+ end
17
+
18
+ private
19
+
20
+ def should_exclude?(event)
21
+ return false unless event.exception
22
+
23
+ exception_class = event.exception.class
24
+ exception_message = event.exception.message
25
+
26
+ # Check exact class matches
27
+ return true if @excluded_exceptions.any? { |klass| exception_class <= klass }
28
+
29
+ # Check pattern matches
30
+ @excluded_patterns.any? do |pattern|
31
+ case pattern
32
+ when Regexp
33
+ exception_message&.match?(pattern) || exception_class.name.match?(pattern)
34
+ when String
35
+ exception_message&.include?(pattern) || exception_class.name.include?(pattern)
36
+ else
37
+ false
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Middleware
5
+ class MetricsCollector < Base
6
+ def initialize(app, collector: nil)
7
+ super(app)
8
+ @collector = collector
9
+ @metrics = {
10
+ events_processed: 0,
11
+ events_dropped: 0,
12
+ events_by_type: Hash.new(0),
13
+ events_by_level: Hash.new(0)
14
+ }
15
+ @mutex = Mutex.new
16
+ end
17
+
18
+ def call(event, hint = {})
19
+ @mutex.synchronize do
20
+ @metrics[:events_processed] += 1
21
+ @metrics[:events_by_type][event.type] += 1
22
+ @metrics[:events_by_level][event.level] += 1
23
+ end
24
+
25
+ result = @app.call(event, hint)
26
+
27
+ if result.nil?
28
+ @mutex.synchronize do
29
+ @metrics[:events_dropped] += 1
30
+ end
31
+ end
32
+
33
+ # Send to external collector if provided
34
+ @collector&.call(@metrics.dup, event, hint)
35
+
36
+ result
37
+ end
38
+
39
+ def metrics
40
+ @mutex.synchronize { @metrics.dup }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Middleware
5
+ class RateLimiter < Base
6
+ def initialize(app, max_events: 100, time_window: 60)
7
+ super(app)
8
+ @max_events = max_events
9
+ @time_window = time_window
10
+ @events = []
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def call(event, hint = {})
15
+ @mutex.synchronize do
16
+ now = Time.zone.now
17
+ # Remove old events outside time window
18
+ @events.reject! { |timestamp| now - timestamp > @time_window }
19
+
20
+ # Check if we're over the limit
21
+ return nil if @events.length >= @max_events
22
+
23
+ # Add current event
24
+ @events << now
25
+ end
26
+
27
+ @app.call(event, hint)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Middleware
5
+ class ReleaseTracker < Base
6
+ def initialize(app, release_provider: nil)
7
+ super(app)
8
+ @release_provider = release_provider
9
+ end
10
+
11
+ def call(event, hint = {})
12
+ add_release_info(event, hint)
13
+ @app.call(event, hint)
14
+ end
15
+
16
+ private
17
+
18
+ def add_release_info(event, hint)
19
+ release_info = @release_provider&.call(event, hint) || auto_detect_release
20
+
21
+ event.context[:release] = release_info if release_info
22
+ end
23
+
24
+ def auto_detect_release
25
+ release_info = {}
26
+
27
+ # Try to detect Git information
28
+ if git_info = detect_git_info
29
+ release_info.merge!(git_info)
30
+ end
31
+
32
+ # Try to detect deployment info
33
+ if deployment_info = detect_deployment_info
34
+ release_info.merge!(deployment_info)
35
+ end
36
+
37
+ release_info.empty? ? nil : release_info
38
+ end
39
+
40
+ def detect_git_info
41
+ return nil unless File.exist?(".git")
42
+
43
+ begin
44
+ # Get current commit SHA
45
+ commit_sha = `git rev-parse HEAD`.strip
46
+ return nil if commit_sha.empty?
47
+
48
+ # Get branch name
49
+ branch = `git rev-parse --abbrev-ref HEAD`.strip
50
+ branch = nil if branch.empty? || branch == "HEAD"
51
+
52
+ # Get commit timestamp
53
+ commit_time = `git log -1 --format=%ct`.strip
54
+ commit_timestamp = commit_time.empty? ? nil : Time.zone.at(commit_time.to_i)
55
+
56
+ # Get tag if on a tag
57
+ tag = `git describe --exact-match --tags HEAD 2>/dev/null`.strip
58
+ tag = nil if tag.empty?
59
+
60
+ {
61
+ commit_sha: commit_sha,
62
+ branch: branch,
63
+ tag: tag,
64
+ commit_timestamp: commit_timestamp
65
+ }.compact
66
+ rescue StandardError
67
+ nil
68
+ end
69
+ end
70
+
71
+ def detect_deployment_info
72
+ info = {}
73
+
74
+ # Check common deployment environment variables
75
+ info[:deployment_id] = ENV["DEPLOYMENT_ID"] if ENV["DEPLOYMENT_ID"]
76
+ info[:build_number] = ENV["BUILD_NUMBER"] if ENV["BUILD_NUMBER"]
77
+ info[:deployment_time] = parse_deployment_time(ENV["DEPLOYMENT_TIME"]) if ENV["DEPLOYMENT_TIME"]
78
+
79
+ # Check Heroku
80
+ if ENV["HEROKU_APP_NAME"]
81
+ info[:platform] = "heroku"
82
+ info[:app_name] = ENV["HEROKU_APP_NAME"]
83
+ info[:dyno] = ENV.fetch("DYNO", nil)
84
+ info[:slug_commit] = ENV.fetch("HEROKU_SLUG_COMMIT", nil)
85
+ end
86
+
87
+ # Check AWS
88
+ if ENV["AWS_EXECUTION_ENV"]
89
+ info[:platform] = "aws"
90
+ info[:execution_env] = ENV["AWS_EXECUTION_ENV"]
91
+ info[:region] = ENV.fetch("AWS_REGION", nil)
92
+ end
93
+
94
+ # Check Docker
95
+ if ENV["DOCKER_CONTAINER_ID"] || File.exist?("/.dockerenv")
96
+ info[:platform] = "docker"
97
+ info[:container_id] = ENV["DOCKER_CONTAINER_ID"]
98
+ end
99
+
100
+ # Check Kubernetes
101
+ if ENV["KUBERNETES_SERVICE_HOST"]
102
+ info[:platform] = "kubernetes"
103
+ info[:namespace] = ENV.fetch("KUBERNETES_NAMESPACE", nil)
104
+ info[:pod_name] = ENV.fetch("HOSTNAME", nil)
105
+ end
106
+
107
+ info.empty? ? nil : info
108
+ end
109
+
110
+ def parse_deployment_time(time_str)
111
+ Time.zone.parse(time_str)
112
+ rescue StandardError
113
+ nil
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Middleware
5
+ class SampleFilter < Base
6
+ def initialize(app, sample_rate: 1.0, sample_callback: nil)
7
+ super(app)
8
+ @sample_rate = sample_rate
9
+ @sample_callback = sample_callback
10
+ end
11
+
12
+ def call(event, hint = {})
13
+ # Apply custom sampling logic first
14
+ return nil if @sample_callback && !@sample_callback.call(event, hint)
15
+
16
+ # Apply rate-based sampling
17
+ return nil if (@sample_rate < 1.0) && (rand > @sample_rate)
18
+
19
+ @app.call(event, hint)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Middleware
5
+ class SamplingMiddleware < Base
6
+ def initialize(app, sampler)
7
+ super(app)
8
+ @sampler = sampler
9
+ end
10
+
11
+ def call(event, hint = {})
12
+ return nil unless @sampler.sample?(event, hint)
13
+
14
+ @app.call(event, hint)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Middleware
5
+ class UserContextEnhancer < Base
6
+ def initialize(app, user_provider: nil, privacy_mode: false)
7
+ super(app)
8
+ @user_provider = user_provider
9
+ @privacy_mode = privacy_mode
10
+ end
11
+
12
+ def call(event, hint = {})
13
+ enhance_user_context(event, hint)
14
+ @app.call(event, hint)
15
+ end
16
+
17
+ private
18
+
19
+ def enhance_user_context(event, hint)
20
+ # Get user from provider if available
21
+ user_data = @user_provider&.call(event, hint) || {}
22
+
23
+ # Merge with existing user context
24
+ existing_user = event.context[:user] || {}
25
+ enhanced_user = existing_user.merge(user_data)
26
+
27
+ # Apply privacy filtering if enabled
28
+ enhanced_user = apply_privacy_filtering(enhanced_user) if @privacy_mode
29
+
30
+ event.context[:user] = enhanced_user unless enhanced_user.empty?
31
+ end
32
+
33
+ def apply_privacy_filtering(user_data)
34
+ # Remove sensitive fields in privacy mode
35
+ sensitive_fields = %i[email phone address ssn credit_card]
36
+ filtered = user_data.dup
37
+
38
+ sensitive_fields.each do |field|
39
+ filtered[field] = "[FILTERED]" if filtered.key?(field)
40
+ end
41
+
42
+ filtered
43
+ end
44
+ end
45
+ end
46
+ end
@@ -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
@@ -58,70 +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, *, **)
119
- @pipeline.use(middleware_class, *, **)
120
- self
121
- end
122
-
123
- delegate :build, to: :@pipeline
124
-
125
- attr_reader :pipeline
126
- end
127
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") && defined?(Redis::CannotConnectError) && 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