lapsoss 0.1.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.
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "validators"
4
+ require "active_support/configurable"
5
+
6
+ module Lapsoss
7
+ class Configuration
8
+ include Validators
9
+ include ActiveSupport::Configurable
10
+
11
+ attr_accessor :async, :logger, :enabled, :release,
12
+ :scrub_fields, :scrub_all, :whitelist_fields, :randomize_scrub_length,
13
+ :transport_jitter, :fingerprint_patterns,
14
+ :normalize_fingerprint_paths, :normalize_fingerprint_ids, :fingerprint_include_environment,
15
+ :backtrace_context_lines, :backtrace_in_app_patterns, :backtrace_exclude_patterns,
16
+ :backtrace_strip_load_path, :backtrace_max_frames, :backtrace_enable_code_context,
17
+ :enable_pipeline, :pipeline_builder, :sampling_strategy,
18
+ :skip_rails_cache_errors
19
+ attr_reader :fingerprint_callback
20
+ attr_reader :environment, :before_send, :sample_rate, :error_handler,
21
+ :transport_timeout, :transport_max_retries, :transport_initial_backoff,
22
+ :transport_max_backoff, :transport_backoff_multiplier, :transport_ssl_verify
23
+ attr_reader :default_context, :adapter_configs
24
+
25
+ def initialize
26
+ @adapter_configs = {}
27
+ @async = true
28
+ @logger = nil
29
+ @environment = nil
30
+ @enabled = true
31
+ @release = nil
32
+ @before_send = nil
33
+ @sample_rate = 1.0
34
+ @default_context = {}
35
+ @error_handler = nil
36
+ @scrub_fields = nil # Will use defaults from Scrubber
37
+ @scrub_all = false
38
+ @whitelist_fields = []
39
+ @randomize_scrub_length = false
40
+ # Transport reliability settings
41
+ @transport_timeout = 5
42
+ @transport_max_retries = 3
43
+ @transport_initial_backoff = 1.0
44
+ @transport_max_backoff = 64.0
45
+ @transport_backoff_multiplier = 2.0
46
+ @transport_jitter = true
47
+ @transport_ssl_verify = true
48
+ # Fingerprinting settings
49
+ @fingerprint_callback = nil
50
+ @fingerprint_patterns = nil # Will use defaults from Fingerprinter
51
+ @normalize_fingerprint_paths = true
52
+ @normalize_fingerprint_ids = true
53
+ @fingerprint_include_environment = false
54
+ # Backtrace processing settings
55
+ @backtrace_context_lines = 3
56
+ @backtrace_in_app_patterns = []
57
+ @backtrace_exclude_patterns = []
58
+ @backtrace_strip_load_path = true
59
+ @backtrace_max_frames = 100
60
+ @backtrace_enable_code_context = true
61
+ # Pipeline settings
62
+ @enable_pipeline = true
63
+ @pipeline_builder = nil
64
+ @sampling_strategy = nil
65
+ # Rails error filtering
66
+ @skip_rails_cache_errors = true
67
+ end
68
+
69
+
70
+ # Register a named adapter configuration
71
+ #
72
+ # @param name [Symbol] Unique name for this adapter instance
73
+ # @param type [Symbol] The adapter type (e.g., :sentry, :appsignal)
74
+ # @param settings [Hash] Configuration settings for the adapter
75
+ def register_adapter(name, type, **settings)
76
+ @adapter_configs[name.to_sym] = {
77
+ type: type&.to_sym,
78
+ settings: settings
79
+ }
80
+ end
81
+
82
+ # Convenience method for Sentry
83
+ def use_sentry(name: :sentry, **settings)
84
+ register_adapter(name, :sentry, **settings)
85
+ end
86
+
87
+ # Convenience method for AppSignal
88
+ def use_appsignal(name: :appsignal, **settings)
89
+ register_adapter(name, :appsignal, **settings)
90
+ end
91
+
92
+ # Convenience method for Insight Hub
93
+ def use_insight_hub(name: :insight_hub, **settings)
94
+ register_adapter(name, :insight_hub, **settings)
95
+ end
96
+
97
+ # Backwards compatibility for Bugsnag
98
+ def use_bugsnag(name: :bugsnag, **settings)
99
+ register_adapter(name, :bugsnag, **settings)
100
+ end
101
+
102
+
103
+ # Convenience method for Rollbar
104
+ def use_rollbar(name: :rollbar, **settings)
105
+ register_adapter(name, :rollbar, **settings)
106
+ end
107
+
108
+ # Convenience method for Logger
109
+ def use_logger(name: :logger, **settings)
110
+ register_adapter(name, :logger, **settings)
111
+ end
112
+
113
+ # Apply configuration by registering all adapters
114
+ def apply!
115
+ Registry.instance.clear!
116
+
117
+ @adapter_configs.each do |name, config|
118
+ Registry.instance.register(
119
+ name,
120
+ config[:type],
121
+ **config[:settings]
122
+ )
123
+ end
124
+ end
125
+
126
+ # Check if any adapters are configured
127
+ def adapters_configured?
128
+ !@adapter_configs.empty?
129
+ end
130
+
131
+ # Get configured adapter names
132
+ def adapter_names
133
+ @adapter_configs.keys
134
+ end
135
+
136
+ # Default tags setter/getter
137
+ def default_tags=(tags)
138
+ @default_context[:tags] = tags
139
+ end
140
+
141
+ def default_tags
142
+ @default_context[:tags] ||= {}
143
+ end
144
+
145
+ # Default user setter/getter
146
+ def default_user=(user)
147
+ @default_context[:user] = user
148
+ end
149
+
150
+ def default_user
151
+ @default_context[:user]
152
+ end
153
+
154
+ # Default extra context setter/getter
155
+ def default_extra=(extra)
156
+ @default_context[:extra] = extra
157
+ end
158
+
159
+ def default_extra
160
+ @default_context[:extra] ||= {}
161
+ end
162
+
163
+ def clear!
164
+ initialize
165
+ end
166
+
167
+ # Pipeline configuration
168
+ def configure_pipeline(&block)
169
+ require_relative "pipeline"
170
+ @pipeline_builder = PipelineBuilder.new
171
+ yield(@pipeline_builder) if block_given?
172
+ @pipeline_builder
173
+ end
174
+
175
+ def pipeline
176
+ @pipeline_builder&.pipeline
177
+ end
178
+
179
+ # Sampling configuration
180
+ def configure_sampling(strategy = nil, &block)
181
+ require_relative "sampling"
182
+
183
+ if strategy
184
+ @sampling_strategy = strategy
185
+ elsif block_given?
186
+ @sampling_strategy = block
187
+ end
188
+ end
189
+
190
+ def create_sampling_strategy
191
+ require_relative "sampling"
192
+
193
+ case @sampling_strategy
194
+ when Symbol
195
+ case @sampling_strategy
196
+ when :production
197
+ Sampling::SamplingFactory.create_production_sampling
198
+ when :development
199
+ Sampling::SamplingFactory.create_development_sampling
200
+ when :user_focused
201
+ Sampling::SamplingFactory.create_user_focused_sampling
202
+ else
203
+ Sampling::UniformSampler.new(@sample_rate)
204
+ end
205
+ when Proc
206
+ @sampling_strategy
207
+ when nil
208
+ Sampling::UniformSampler.new(@sample_rate)
209
+ else
210
+ @sampling_strategy
211
+ end
212
+ end
213
+
214
+ # Validation and setter overrides
215
+ def sample_rate=(value)
216
+ validate_sample_rate!(value, "sample_rate") if value
217
+ @sample_rate = value
218
+ end
219
+
220
+ def before_send=(value)
221
+ validate_callable!(value, "before_send")
222
+ @before_send = value
223
+ end
224
+
225
+ def error_handler=(value)
226
+ validate_callable!(value, "error_handler")
227
+ @error_handler = value
228
+ end
229
+
230
+ def environment=(value)
231
+ validate_environment!(value, "environment") if value
232
+ @environment = value&.to_s
233
+ end
234
+
235
+ def transport_timeout=(value)
236
+ validate_timeout!(value, "transport_timeout") if value
237
+ @transport_timeout = value
238
+ end
239
+
240
+ def transport_max_retries=(value)
241
+ validate_retries!(value, "transport_max_retries") if value
242
+ @transport_max_retries = value
243
+ end
244
+
245
+ def transport_initial_backoff=(value)
246
+ validate_timeout!(value, "transport_initial_backoff") if value
247
+ @transport_initial_backoff = value
248
+ end
249
+
250
+ def transport_max_backoff=(value)
251
+ validate_timeout!(value, "transport_max_backoff") if value
252
+ @transport_max_backoff = value
253
+ end
254
+
255
+ def transport_backoff_multiplier=(value)
256
+ if value
257
+ validate_type!(value, [Numeric], "transport_backoff_multiplier")
258
+ validate_numeric_range!(value, 1.0..10.0, "transport_backoff_multiplier")
259
+ end
260
+ @transport_backoff_multiplier = value
261
+ end
262
+
263
+ def transport_ssl_verify=(value)
264
+ validate_boolean!(value, "transport_ssl_verify") if value
265
+ @transport_ssl_verify = value
266
+ end
267
+
268
+ def fingerprint_callback=(value)
269
+ validate_callable!(value, "fingerprint_callback")
270
+ @fingerprint_callback = value
271
+ end
272
+
273
+ # Configuration validation
274
+ def validate!
275
+ validate_sample_rate!(@sample_rate, "sample_rate") if @sample_rate
276
+ validate_callable!(@before_send, "before_send")
277
+ validate_callable!(@error_handler, "error_handler")
278
+ validate_callable!(@fingerprint_callback, "fingerprint_callback")
279
+ validate_environment!(@environment, "environment") if @environment
280
+
281
+ # Validate transport settings
282
+ validate_timeout!(@transport_timeout, "transport_timeout")
283
+ validate_retries!(@transport_max_retries, "transport_max_retries")
284
+ validate_timeout!(@transport_initial_backoff, "transport_initial_backoff")
285
+ validate_timeout!(@transport_max_backoff, "transport_max_backoff")
286
+
287
+ if @transport_backoff_multiplier
288
+ validate_type!(@transport_backoff_multiplier, [Numeric], "transport_backoff_multiplier")
289
+ validate_numeric_range!(@transport_backoff_multiplier, 1.0..10.0, "transport_backoff_multiplier")
290
+ end
291
+
292
+ # Validate that initial backoff is less than max backoff
293
+ if @transport_initial_backoff && @transport_max_backoff && @transport_initial_backoff > @transport_max_backoff
294
+ raise ValidationError, "transport_initial_backoff (#{@transport_initial_backoff}) must be less than transport_max_backoff (#{@transport_max_backoff})"
295
+ end
296
+
297
+ # Validate adapter configurations
298
+ @adapter_configs.each do |name, config|
299
+ validate_adapter_config!(name, config)
300
+ end
301
+ end
302
+
303
+ private
304
+
305
+ def validate_adapter_config!(name, config)
306
+ validate_presence!(config[:type], "adapter type for '#{name}'")
307
+ validate_type!(config[:settings], [Hash], "adapter settings for '#{name}'")
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+
5
+ module Lapsoss
6
+ class Current < ActiveSupport::CurrentAttributes
7
+ attribute :scope
8
+ end
9
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "scrubber"
4
+ require_relative "fingerprinter"
5
+ require_relative "backtrace_processor"
6
+
7
+ module Lapsoss
8
+ class Event
9
+ attr_accessor :type, :timestamp, :level, :message, :exception, :context, :environment, :fingerprint, :backtrace_frames
10
+
11
+ def initialize(type:, level: :info, **attributes)
12
+ @type = type
13
+ @level = level
14
+ @timestamp = Time.now
15
+ @context = {}
16
+ @environment = Lapsoss.configuration.environment
17
+
18
+ attributes.each do |key, value|
19
+ instance_variable_set("@#{key}", value) if respond_to?("#{key}=")
20
+ end
21
+
22
+ # Process backtrace if we have an exception
23
+ @backtrace_frames = process_backtrace if @exception
24
+
25
+ # Generate fingerprint after all attributes are set (unless explicitly set to nil or a value)
26
+ @fingerprint = generate_fingerprint if @fingerprint.nil? && !attributes.key?(:fingerprint)
27
+ end
28
+
29
+ def to_h
30
+ data = {
31
+ type: type,
32
+ timestamp: timestamp,
33
+ level: level,
34
+ message: message,
35
+ exception: exception_data,
36
+ backtrace: backtrace_data,
37
+ context: context,
38
+ environment: environment,
39
+ fingerprint: fingerprint
40
+ }.compact
41
+
42
+ scrub_sensitive_data(data)
43
+ end
44
+
45
+ def scrub_sensitive_data(data)
46
+ config = Lapsoss.configuration
47
+
48
+ scrubber = Scrubber.new(
49
+ scrub_fields: config.scrub_fields,
50
+ scrub_all: config.scrub_all,
51
+ whitelist_fields: config.whitelist_fields,
52
+ randomize_scrub_length: config.randomize_scrub_length
53
+ )
54
+
55
+ scrubber.scrub(data)
56
+ end
57
+
58
+ def generate_fingerprint
59
+ config = Lapsoss.configuration
60
+
61
+ fingerprinter = Fingerprinter.new(
62
+ custom_callback: config.fingerprint_callback,
63
+ patterns: config.fingerprint_patterns,
64
+ normalize_paths: config.normalize_fingerprint_paths,
65
+ normalize_ids: config.normalize_fingerprint_ids,
66
+ include_environment: config.fingerprint_include_environment
67
+ )
68
+
69
+ fingerprinter.generate_fingerprint(self)
70
+ end
71
+
72
+ def process_backtrace
73
+ return nil unless @exception&.backtrace
74
+
75
+ config = Lapsoss.configuration
76
+
77
+ processor = BacktraceProcessor.new(
78
+ context_lines: config.backtrace_context_lines,
79
+ max_frames: config.backtrace_max_frames,
80
+ enable_code_context: config.backtrace_enable_code_context,
81
+ strip_load_path: config.backtrace_strip_load_path,
82
+ in_app_patterns: config.backtrace_in_app_patterns,
83
+ exclude_patterns: config.backtrace_exclude_patterns
84
+ )
85
+
86
+ processor.process_exception_backtrace(@exception)
87
+ end
88
+
89
+ private
90
+
91
+ def exception_data
92
+ return nil unless exception
93
+
94
+ {
95
+ class: exception.class.name,
96
+ message: exception.message,
97
+ backtrace: exception.backtrace&.first(20)
98
+ }
99
+ end
100
+
101
+ def backtrace_data
102
+ return nil unless @backtrace_frames&.any?
103
+
104
+ @backtrace_frames.map(&:to_h)
105
+ end
106
+ end
107
+ end