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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +855 -0
- data/lib/lapsoss/adapters/appsignal_adapter.rb +136 -0
- data/lib/lapsoss/adapters/base.rb +88 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +190 -0
- data/lib/lapsoss/adapters/logger_adapter.rb +67 -0
- data/lib/lapsoss/adapters/rollbar_adapter.rb +157 -0
- data/lib/lapsoss/adapters/sentry_adapter.rb +197 -0
- data/lib/lapsoss/backtrace_frame.rb +258 -0
- data/lib/lapsoss/backtrace_processor.rb +346 -0
- data/lib/lapsoss/client.rb +115 -0
- data/lib/lapsoss/configuration.rb +310 -0
- data/lib/lapsoss/current.rb +9 -0
- data/lib/lapsoss/event.rb +107 -0
- data/lib/lapsoss/exclusions.rb +429 -0
- data/lib/lapsoss/fingerprinter.rb +217 -0
- data/lib/lapsoss/http_client.rb +79 -0
- data/lib/lapsoss/middleware.rb +353 -0
- data/lib/lapsoss/pipeline.rb +131 -0
- data/lib/lapsoss/railtie.rb +72 -0
- data/lib/lapsoss/registry.rb +114 -0
- data/lib/lapsoss/release_tracker.rb +553 -0
- data/lib/lapsoss/router.rb +36 -0
- data/lib/lapsoss/sampling.rb +332 -0
- data/lib/lapsoss/scope.rb +110 -0
- data/lib/lapsoss/scrubber.rb +170 -0
- data/lib/lapsoss/user_context.rb +355 -0
- data/lib/lapsoss/validators.rb +142 -0
- data/lib/lapsoss/version.rb +5 -0
- data/lib/lapsoss.rb +76 -0
- metadata +217 -0
|
@@ -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,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
|