lapsoss 0.4.9 → 0.4.11
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 +91 -11
- data/lib/lapsoss/adapters/concerns/http_delivery.rb +19 -7
- data/lib/lapsoss/adapters/concerns/level_mapping.rb +10 -0
- data/lib/lapsoss/adapters/concerns/stacktrace_builder.rb +70 -0
- data/lib/lapsoss/adapters/concerns/trace_context.rb +52 -0
- data/lib/lapsoss/adapters/openobserve_adapter.rb +156 -0
- data/lib/lapsoss/adapters/otlp_adapter.rb +231 -0
- data/lib/lapsoss/adapters/rollbar_adapter.rb +4 -2
- data/lib/lapsoss/adapters/sentry_adapter.rb +2 -2
- data/lib/lapsoss/adapters/telebugs_adapter.rb +4 -4
- data/lib/lapsoss/client.rb +26 -8
- data/lib/lapsoss/configuration.rb +33 -3
- data/lib/lapsoss/middleware/sample_filter.rb +32 -0
- data/lib/lapsoss/middleware/user_context_enhancer.rb +44 -0
- data/lib/lapsoss/router.rb +3 -2
- data/lib/lapsoss/scrubber.rb +90 -2
- data/lib/lapsoss/version.rb +1 -1
- data/lib/lapsoss.rb +18 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4370921854bb3be19bd9209a54da4a763833386b0ef917c6ec5a632a2941db9
|
|
4
|
+
data.tar.gz: 7df3efd16d2b889561b94a76c2d8ed7b7e764ca6b2a5b45e3edf510b58bcf8bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23f1efbcc3d5fb6cceb32f9d7794f1ce8943dfe51696b42c95c2bdc257acb0a6770c0470d2d72729f45e9beb7919948c46dea8db6bfb903381466c7dfd2ab771
|
|
7
|
+
data.tar.gz: 3fd7dbd3eca37712e7eec4fa9908939b797805d779d62f242c6c6e30f1ca7baeeb49470f1a1f7a1cd97f5d3d0320ecd116048870a2bb4d530380bcfd716243e5
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Lapsoss - Vendor-Neutral Error Reporting for Rails
|
|
2
2
|
|
|
3
|
+
[](https://github.com/seuros/lapsoss/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/lapsoss)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://rubygems.org/gems/lapsoss)
|
|
7
|
+
|
|
3
8
|
## The Problem We All Face
|
|
4
9
|
|
|
5
10
|
You're 6 months into production with Bugsnag. The CFO says "costs are too high, switch to Sentry."
|
|
@@ -78,7 +83,7 @@ end
|
|
|
78
83
|
|
|
79
84
|
**Option 1: Automatic Rails.error Integration (Recommended)**
|
|
80
85
|
```ruby
|
|
81
|
-
# config/initializers/lapsoss.rb
|
|
86
|
+
# config/initializers/lapsoss.rb
|
|
82
87
|
Lapsoss.configure do |config|
|
|
83
88
|
config.use_appsignal(push_api_key: ENV['APPSIGNAL_KEY'])
|
|
84
89
|
end
|
|
@@ -141,7 +146,7 @@ end
|
|
|
141
146
|
# Old: Bugsnag.notify(e)
|
|
142
147
|
# New: Lapsoss.capture_exception(e)
|
|
143
148
|
|
|
144
|
-
# Step 4: Remove bugsnag gem when ready
|
|
149
|
+
# Step 4: Remove bugsnag gem when ready
|
|
145
150
|
# Your app keeps running, now on Telebugs
|
|
146
151
|
```
|
|
147
152
|
|
|
@@ -215,6 +220,8 @@ All adapters are pure Ruby implementations with no external SDK dependencies:
|
|
|
215
220
|
- **AppSignal** - Error tracking and deploy markers
|
|
216
221
|
- **Insight Hub** (formerly Bugsnag) - Error tracking with breadcrumbs
|
|
217
222
|
- **Telebugs** - Sentry-compatible protocol (perfect for self-hosted alternatives)
|
|
223
|
+
- **OpenObserve** - Open-source observability platform (logs, metrics, traces)
|
|
224
|
+
- **OTLP** - OpenTelemetry Protocol (works with SigNoz, Jaeger, Tempo, Honeycomb, etc.)
|
|
218
225
|
|
|
219
226
|
## Configuration
|
|
220
227
|
|
|
@@ -249,6 +256,39 @@ Lapsoss.configure do |config|
|
|
|
249
256
|
end
|
|
250
257
|
```
|
|
251
258
|
|
|
259
|
+
### Using OpenObserve
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# OpenObserve - open-source observability platform
|
|
263
|
+
Lapsoss.configure do |config|
|
|
264
|
+
config.use_openobserve(
|
|
265
|
+
endpoint: ENV['OPENOBSERVE_ENDPOINT'], # e.g., "http://localhost:5080"
|
|
266
|
+
username: ENV['OPENOBSERVE_USERNAME'],
|
|
267
|
+
password: ENV['OPENOBSERVE_PASSWORD'],
|
|
268
|
+
org: "default", # optional, defaults to "default"
|
|
269
|
+
stream: "errors" # optional, defaults to "errors"
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Using OTLP (OpenTelemetry Protocol)
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
# Works with SigNoz, Jaeger, Tempo, Honeycomb, Datadog, etc.
|
|
278
|
+
Lapsoss.configure do |config|
|
|
279
|
+
config.use_otlp(
|
|
280
|
+
endpoint: ENV['OTLP_ENDPOINT'], # e.g., "http://localhost:4318"
|
|
281
|
+
service_name: "my-rails-app",
|
|
282
|
+
headers: { "X-Custom-Header" => "value" } # optional custom headers
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Or use convenience helpers for specific services
|
|
286
|
+
config.use_signoz(signoz_api_key: ENV['SIGNOZ_API_KEY'])
|
|
287
|
+
config.use_jaeger(endpoint: "http://jaeger:4318")
|
|
288
|
+
config.use_tempo(endpoint: "http://tempo:4318")
|
|
289
|
+
end
|
|
290
|
+
```
|
|
291
|
+
|
|
252
292
|
### Advanced Configuration
|
|
253
293
|
|
|
254
294
|
```ruby
|
|
@@ -264,13 +304,29 @@ Lapsoss.configure do |config|
|
|
|
264
304
|
|
|
265
305
|
# Sampling (see docs/sampling_strategies.md for advanced examples)
|
|
266
306
|
config.sample_rate = Rails.env.production? ? 0.25 : 1.0
|
|
267
|
-
|
|
307
|
+
|
|
268
308
|
# Transport settings
|
|
269
309
|
config.transport_timeout = 10 # seconds
|
|
270
310
|
config.transport_max_retries = 3
|
|
271
311
|
end
|
|
272
312
|
```
|
|
273
313
|
|
|
314
|
+
### Pipeline & Sampling (optional)
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
Lapsoss.configure do |config|
|
|
318
|
+
# Build a middleware pipeline for every event
|
|
319
|
+
config.configure_pipeline do |pipeline|
|
|
320
|
+
pipeline.sample(rate: 0.1) # Drop 90% of events
|
|
321
|
+
|
|
322
|
+
pipeline.enhance_user_context(
|
|
323
|
+
provider: ->(event, _) { current_user&.slice(:id, :email) },
|
|
324
|
+
privacy_mode: true # keep only ids
|
|
325
|
+
)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
274
330
|
### Filtering Errors
|
|
275
331
|
|
|
276
332
|
You decide what errors to track. Lapsoss doesn't make assumptions:
|
|
@@ -283,7 +339,7 @@ Lapsoss.configure do |config|
|
|
|
283
339
|
return nil if event.exception.is_a?(ActiveRecord::RecordNotFound)
|
|
284
340
|
event
|
|
285
341
|
end
|
|
286
|
-
|
|
342
|
+
|
|
287
343
|
# Or use the exclusion filter for more complex rules
|
|
288
344
|
config.exclusion_filter = Lapsoss::ExclusionFilter.new(
|
|
289
345
|
# Exclude specific exception types
|
|
@@ -291,20 +347,20 @@ Lapsoss.configure do |config|
|
|
|
291
347
|
"ActionController::RoutingError", # Your choice
|
|
292
348
|
"ActiveRecord::RecordNotFound" # Your decision
|
|
293
349
|
],
|
|
294
|
-
|
|
350
|
+
|
|
295
351
|
# Exclude by pattern matching
|
|
296
352
|
excluded_patterns: [
|
|
297
353
|
/timeout/i, # If timeouts are expected in your app
|
|
298
354
|
/user not found/i # If these are normal in your workflow
|
|
299
355
|
],
|
|
300
|
-
|
|
356
|
+
|
|
301
357
|
# Exclude specific error messages
|
|
302
358
|
excluded_messages: [
|
|
303
359
|
"No route matches",
|
|
304
360
|
"Invalid authenticity token"
|
|
305
361
|
]
|
|
306
362
|
)
|
|
307
|
-
|
|
363
|
+
|
|
308
364
|
# Add custom exclusion logic
|
|
309
365
|
config.exclusion_filter.add_exclusion(:custom, lambda do |event|
|
|
310
366
|
# Your business logic here
|
|
@@ -346,6 +402,30 @@ Rails.application.config.filter_parameters += [:password, :token]
|
|
|
346
402
|
# Lapsoss automatically uses these filters - no additional configuration needed!
|
|
347
403
|
```
|
|
348
404
|
|
|
405
|
+
Additional controls:
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
Lapsoss.configure do |config|
|
|
409
|
+
config.scrub_fields = %w[credit_card ssn api_key]
|
|
410
|
+
config.scrub_all = true # Mask everything by default
|
|
411
|
+
config.whitelist_fields = %w[user_id request_id] # Keep these fields as-is
|
|
412
|
+
config.randomize_scrub_length = true # Avoid fixed "[FILTERED]" marker
|
|
413
|
+
end
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Error Handler Hook
|
|
417
|
+
|
|
418
|
+
Get a callback when an adapter fails to deliver:
|
|
419
|
+
|
|
420
|
+
```ruby
|
|
421
|
+
Lapsoss.configure do |config|
|
|
422
|
+
config.error_handler = lambda do |adapter, event, error|
|
|
423
|
+
Rails.logger.error("Delivery failed for #{adapter.name}: #{error.message}")
|
|
424
|
+
Rails.logger.error(event.to_h) if Rails.env.development?
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
```
|
|
428
|
+
|
|
349
429
|
### Custom Fingerprinting
|
|
350
430
|
|
|
351
431
|
Control how errors are grouped:
|
|
@@ -393,7 +473,7 @@ class Liberation
|
|
|
393
473
|
end
|
|
394
474
|
puts "✅ Continued execution after error"
|
|
395
475
|
end
|
|
396
|
-
|
|
476
|
+
|
|
397
477
|
def self.revolt!
|
|
398
478
|
Rails.error.record do
|
|
399
479
|
raise RuntimeError, "Revolution cannot be stopped!"
|
|
@@ -464,7 +544,7 @@ end
|
|
|
464
544
|
|
|
465
545
|
# These methods mirror Rails.error exactly:
|
|
466
546
|
# - Lapsoss.handle → Rails.error.handle
|
|
467
|
-
# - Lapsoss.record → Rails.error.record
|
|
547
|
+
# - Lapsoss.record → Rails.error.record
|
|
468
548
|
# - Lapsoss.report → Rails.error.report
|
|
469
549
|
```
|
|
470
550
|
|
|
@@ -492,9 +572,9 @@ class TelebugsAdapter < Lapsoss::Adapters::SentryAdapter
|
|
|
492
572
|
def initialize(name = :telebugs, settings = {})
|
|
493
573
|
super(name, settings)
|
|
494
574
|
end
|
|
495
|
-
|
|
575
|
+
|
|
496
576
|
private
|
|
497
|
-
|
|
577
|
+
|
|
498
578
|
def build_headers(public_key)
|
|
499
579
|
super(public_key).merge(
|
|
500
580
|
"X-Telebugs-Client" => "lapsoss/#{Lapsoss::VERSION}"
|
|
@@ -12,8 +12,8 @@ module Lapsoss
|
|
|
12
12
|
extend ActiveSupport::Concern
|
|
13
13
|
|
|
14
14
|
included do
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# Instance-level endpoint configuration for adapter isolation
|
|
16
|
+
attr_reader :api_endpoint, :api_path
|
|
17
17
|
|
|
18
18
|
# Memoized git info using AS
|
|
19
19
|
mattr_accessor :git_info_cache, default: {}
|
|
@@ -39,7 +39,7 @@ module Lapsoss
|
|
|
39
39
|
handle_response(response)
|
|
40
40
|
end
|
|
41
41
|
rescue => error
|
|
42
|
-
handle_delivery_error(error)
|
|
42
|
+
handle_delivery_error(error, event)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Common headers for all adapters
|
|
@@ -101,7 +101,7 @@ module Lapsoss
|
|
|
101
101
|
raise DeliveryError.new("Client error: #{message}", response: response)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def handle_delivery_error(error)
|
|
104
|
+
def handle_delivery_error(error, event = nil)
|
|
105
105
|
ActiveSupport::Notifications.instrument("error.lapsoss",
|
|
106
106
|
adapter: self.class.name,
|
|
107
107
|
error: error.class.name,
|
|
@@ -109,10 +109,22 @@ module Lapsoss
|
|
|
109
109
|
)
|
|
110
110
|
|
|
111
111
|
Lapsoss.configuration.logger&.error("[#{self.class.name}] Delivery failed: #{error.message}")
|
|
112
|
-
Lapsoss.
|
|
112
|
+
Lapsoss.call_error_handler(adapter: self, event: event, error: error)
|
|
113
|
+
mark_error_handled(error)
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
if error.is_a?(DeliveryError)
|
|
116
|
+
raise error
|
|
117
|
+
else
|
|
118
|
+
delivery_error = DeliveryError.new("Delivery failed: #{error.message}", cause: error)
|
|
119
|
+
mark_error_handled(delivery_error)
|
|
120
|
+
raise delivery_error
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def mark_error_handled(error)
|
|
125
|
+
error.instance_variable_set(:@lapsoss_error_handled, true)
|
|
126
|
+
rescue StandardError
|
|
127
|
+
# If setting the flag fails, we still continue
|
|
116
128
|
end
|
|
117
129
|
|
|
118
130
|
private
|
|
@@ -44,6 +44,16 @@ module Lapsoss
|
|
|
44
44
|
error: "error",
|
|
45
45
|
fatal: "error",
|
|
46
46
|
critical: "error"
|
|
47
|
+
}.with_indifferent_access,
|
|
48
|
+
|
|
49
|
+
openobserve: {
|
|
50
|
+
debug: "DEBUG",
|
|
51
|
+
info: "INFO",
|
|
52
|
+
warning: "WARN",
|
|
53
|
+
warn: "WARN",
|
|
54
|
+
error: "ERROR",
|
|
55
|
+
fatal: "FATAL",
|
|
56
|
+
critical: "FATAL"
|
|
47
57
|
}.with_indifferent_access
|
|
48
58
|
}.freeze
|
|
49
59
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
require "active_support/core_ext/object/blank"
|
|
5
|
+
|
|
6
|
+
module Lapsoss
|
|
7
|
+
module Adapters
|
|
8
|
+
module Concerns
|
|
9
|
+
# Shared stacktrace building logic for adapters
|
|
10
|
+
# Provides consistent frame formatting across Sentry, OpenObserve, OTLP, etc.
|
|
11
|
+
module StacktraceBuilder
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
|
|
14
|
+
# Build frames from event backtrace
|
|
15
|
+
# @param event [Lapsoss::Event] The event with backtrace_frames
|
|
16
|
+
# @param reverse [Boolean] Reverse frame order (Sentry expects oldest-to-newest)
|
|
17
|
+
# @return [Array<Hash>] Array of formatted frame hashes
|
|
18
|
+
def build_frames(event, reverse: false)
|
|
19
|
+
return [] unless event.has_backtrace?
|
|
20
|
+
|
|
21
|
+
frames = event.backtrace_frames.map { |frame| build_frame(frame) }
|
|
22
|
+
reverse ? frames.reverse : frames
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Build a single frame hash from a BacktraceFrame
|
|
26
|
+
# @param frame [Lapsoss::BacktraceFrame] The frame to format
|
|
27
|
+
# @return [Hash] Formatted frame hash
|
|
28
|
+
def build_frame(frame)
|
|
29
|
+
frame_hash = {
|
|
30
|
+
filename: frame.filename,
|
|
31
|
+
abs_path: frame.absolute_path || frame.filename,
|
|
32
|
+
function: frame.method_name || frame.function,
|
|
33
|
+
lineno: frame.line_number,
|
|
34
|
+
in_app: frame.in_app
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
add_code_context(frame_hash, frame) if frame.code_context.present?
|
|
38
|
+
|
|
39
|
+
frame_hash.compact
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Build raw stacktrace string for logging/simple formats
|
|
43
|
+
# @param event [Lapsoss::Event] The event with backtrace_frames
|
|
44
|
+
# @return [Array<String>] Array of formatted frame strings
|
|
45
|
+
def build_raw_stacktrace(event)
|
|
46
|
+
return [] unless event.has_backtrace?
|
|
47
|
+
|
|
48
|
+
event.backtrace_frames.map do |frame|
|
|
49
|
+
"#{frame.absolute_path || frame.filename}:#{frame.line_number} in `#{frame.method_name}`"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Build stacktrace as single string (for OTLP exception.stacktrace)
|
|
54
|
+
# @param event [Lapsoss::Event] The event with backtrace_frames
|
|
55
|
+
# @return [String] Newline-separated stacktrace
|
|
56
|
+
def build_stacktrace_string(event)
|
|
57
|
+
build_raw_stacktrace(event).join("\n")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def add_code_context(frame_hash, frame)
|
|
63
|
+
frame_hash[:pre_context] = frame.code_context[:pre_context]
|
|
64
|
+
frame_hash[:context_line] = frame.code_context[:context_line]
|
|
65
|
+
frame_hash[:post_context] = frame.code_context[:post_context]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Lapsoss
|
|
7
|
+
module Adapters
|
|
8
|
+
module Concerns
|
|
9
|
+
# Trace context utilities for OTLP and distributed tracing
|
|
10
|
+
# Provides trace/span ID generation and timestamp formatting
|
|
11
|
+
module TraceContext
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
|
|
14
|
+
# Generate a W3C Trace Context compliant trace ID (32 hex chars = 128 bits)
|
|
15
|
+
# @return [String] 32 character hex string
|
|
16
|
+
def generate_trace_id
|
|
17
|
+
SecureRandom.hex(16)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Generate a W3C Trace Context compliant span ID (16 hex chars = 64 bits)
|
|
21
|
+
# @return [String] 16 character hex string
|
|
22
|
+
def generate_span_id
|
|
23
|
+
SecureRandom.hex(8)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Convert time to nanoseconds since Unix epoch (OTLP format)
|
|
27
|
+
# @param time [Time] The time to convert
|
|
28
|
+
# @return [Integer] Nanoseconds since Unix epoch
|
|
29
|
+
def timestamp_nanos(time)
|
|
30
|
+
time ||= Time.current
|
|
31
|
+
(time.to_f * 1_000_000_000).to_i
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Convert time to microseconds since Unix epoch (OpenObserve format)
|
|
35
|
+
# @param time [Time] The time to convert
|
|
36
|
+
# @return [Integer] Microseconds since Unix epoch
|
|
37
|
+
def timestamp_micros(time)
|
|
38
|
+
time ||= Time.current
|
|
39
|
+
(time.to_f * 1_000_000).to_i
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Convert time to milliseconds since Unix epoch
|
|
43
|
+
# @param time [Time] The time to convert
|
|
44
|
+
# @return [Integer] Milliseconds since Unix epoch
|
|
45
|
+
def timestamp_millis(time)
|
|
46
|
+
time ||= Time.current
|
|
47
|
+
(time.to_f * 1_000).to_i
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/object/blank"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module Lapsoss
|
|
7
|
+
module Adapters
|
|
8
|
+
# OpenObserve adapter - sends errors as structured JSON logs
|
|
9
|
+
# OpenObserve is an observability platform that accepts logs via simple JSON API
|
|
10
|
+
# Docs: https://openobserve.ai/docs/ingestion/
|
|
11
|
+
class OpenobserveAdapter < Base
|
|
12
|
+
include Concerns::LevelMapping
|
|
13
|
+
include Concerns::HttpDelivery
|
|
14
|
+
|
|
15
|
+
self.level_mapping_type = :openobserve
|
|
16
|
+
|
|
17
|
+
DEFAULT_STREAM = "errors"
|
|
18
|
+
DEFAULT_ORG = "default"
|
|
19
|
+
|
|
20
|
+
def initialize(name, settings = {})
|
|
21
|
+
super
|
|
22
|
+
|
|
23
|
+
@endpoint = settings[:endpoint].presence || ENV["OPENOBSERVE_ENDPOINT"]
|
|
24
|
+
@username = settings[:username].presence || ENV["OPENOBSERVE_USERNAME"]
|
|
25
|
+
@password = settings[:password].presence || ENV["OPENOBSERVE_PASSWORD"]
|
|
26
|
+
@org = settings[:org].presence || ENV["OPENOBSERVE_ORG"] || DEFAULT_ORG
|
|
27
|
+
@stream = settings[:stream].presence || ENV["OPENOBSERVE_STREAM"] || DEFAULT_STREAM
|
|
28
|
+
|
|
29
|
+
if @endpoint.blank? || @username.blank? || @password.blank?
|
|
30
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss::OpenobserveAdapter] Missing endpoint, username or password - adapter disabled"
|
|
31
|
+
@enabled = false
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
setup_endpoint
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def capture(event)
|
|
39
|
+
deliver(event.scrubbed)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def capabilities
|
|
43
|
+
super.merge(
|
|
44
|
+
breadcrumbs: false,
|
|
45
|
+
code_context: true,
|
|
46
|
+
data_scrubbing: true
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def setup_endpoint
|
|
53
|
+
uri = URI.parse(@endpoint)
|
|
54
|
+
@api_endpoint = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
|
55
|
+
@api_path = "/api/#{@org}/#{@stream}/_json"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_payload(event)
|
|
59
|
+
# OpenObserve expects JSON array of log entries
|
|
60
|
+
[ build_log_entry(event) ]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_log_entry(event)
|
|
64
|
+
entry = {
|
|
65
|
+
_timestamp: timestamp_microseconds(event.timestamp),
|
|
66
|
+
level: map_level(event.level),
|
|
67
|
+
logger: "lapsoss",
|
|
68
|
+
environment: event.environment.presence || "production",
|
|
69
|
+
service: @settings[:service_name].presence || "rails"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case event.type
|
|
73
|
+
when :exception
|
|
74
|
+
entry.merge!(build_exception_entry(event))
|
|
75
|
+
when :message
|
|
76
|
+
entry[:message] = event.message
|
|
77
|
+
else
|
|
78
|
+
entry[:message] = event.message || "Unknown event"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Add optional context
|
|
82
|
+
entry[:user] = event.user_context if event.user_context.present?
|
|
83
|
+
entry[:tags] = event.tags if event.tags.present?
|
|
84
|
+
entry[:extra] = event.extra if event.extra.present?
|
|
85
|
+
entry[:request] = event.request_context if event.request_context.present?
|
|
86
|
+
entry[:transaction] = event.transaction if event.transaction.present?
|
|
87
|
+
entry[:fingerprint] = event.fingerprint if event.fingerprint.present?
|
|
88
|
+
|
|
89
|
+
entry.compact_blank
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_exception_entry(event)
|
|
93
|
+
entry = {
|
|
94
|
+
message: "#{event.exception_type}: #{event.exception_message}",
|
|
95
|
+
exception_type: event.exception_type,
|
|
96
|
+
exception_message: event.exception_message
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if event.has_backtrace?
|
|
100
|
+
entry[:stacktrace] = format_stacktrace(event)
|
|
101
|
+
entry[:stacktrace_raw] = event.backtrace_frames.map do |frame|
|
|
102
|
+
"#{frame.absolute_path || frame.filename}:#{frame.line_number} in `#{frame.method_name}`"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
entry
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def format_stacktrace(event)
|
|
110
|
+
event.backtrace_frames.map do |frame|
|
|
111
|
+
frame_entry = {
|
|
112
|
+
filename: frame.filename,
|
|
113
|
+
abs_path: frame.absolute_path || frame.filename,
|
|
114
|
+
function: frame.method_name || frame.function,
|
|
115
|
+
lineno: frame.line_number,
|
|
116
|
+
in_app: frame.in_app
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if frame.code_context.present?
|
|
120
|
+
frame_entry[:context_line] = frame.code_context[:context_line]
|
|
121
|
+
frame_entry[:pre_context] = frame.code_context[:pre_context]
|
|
122
|
+
frame_entry[:post_context] = frame.code_context[:post_context]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
frame_entry.compact
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def timestamp_microseconds(time)
|
|
130
|
+
# OpenObserve expects _timestamp in microseconds
|
|
131
|
+
(time.to_f * 1_000_000).to_i
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def serialize_payload(payload)
|
|
135
|
+
json = ActiveSupport::JSON.encode(payload)
|
|
136
|
+
|
|
137
|
+
if json.bytesize >= compress_threshold
|
|
138
|
+
[ ActiveSupport::Gzip.compress(json), true ]
|
|
139
|
+
else
|
|
140
|
+
[ json, false ]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def compress_threshold
|
|
145
|
+
@settings[:compress_threshold] || 1024
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def adapter_specific_headers
|
|
149
|
+
credentials = Base64.strict_encode64("#{@username}:#{@password}")
|
|
150
|
+
{
|
|
151
|
+
"Authorization" => "Basic #{credentials}"
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/object/blank"
|
|
4
|
+
|
|
5
|
+
module Lapsoss
|
|
6
|
+
module Adapters
|
|
7
|
+
# OTLP adapter - sends errors via OpenTelemetry Protocol
|
|
8
|
+
# Works with any OTLP-compatible backend: SigNoz, Jaeger, Tempo, Honeycomb, etc.
|
|
9
|
+
# Docs: https://opentelemetry.io/docs/specs/otlp/
|
|
10
|
+
class OtlpAdapter < Base
|
|
11
|
+
include Concerns::HttpDelivery
|
|
12
|
+
include Concerns::StacktraceBuilder
|
|
13
|
+
include Concerns::TraceContext
|
|
14
|
+
include Concerns::EnvelopeBuilder
|
|
15
|
+
|
|
16
|
+
# OTLP status codes
|
|
17
|
+
STATUS_CODE_UNSET = 0
|
|
18
|
+
STATUS_CODE_OK = 1
|
|
19
|
+
STATUS_CODE_ERROR = 2
|
|
20
|
+
|
|
21
|
+
# OTLP span kinds
|
|
22
|
+
SPAN_KIND_INTERNAL = 1
|
|
23
|
+
|
|
24
|
+
DEFAULT_ENDPOINT = "http://localhost:4318"
|
|
25
|
+
|
|
26
|
+
def initialize(name, settings = {})
|
|
27
|
+
super
|
|
28
|
+
|
|
29
|
+
@endpoint = settings[:endpoint].presence || ENV["OTLP_ENDPOINT"] || DEFAULT_ENDPOINT
|
|
30
|
+
@headers = settings[:headers] || {}
|
|
31
|
+
@service_name = settings[:service_name].presence || ENV["OTEL_SERVICE_NAME"] || "rails"
|
|
32
|
+
@environment = settings[:environment].presence || ENV["OTEL_ENVIRONMENT"] || "production"
|
|
33
|
+
|
|
34
|
+
# Support common auth patterns
|
|
35
|
+
if (api_key = settings[:api_key].presence || ENV["OTLP_API_KEY"])
|
|
36
|
+
@headers["Authorization"] = "Bearer #{api_key}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if (signoz_key = settings[:signoz_api_key].presence || ENV["SIGNOZ_API_KEY"])
|
|
40
|
+
@headers["signoz-access-token"] = signoz_key
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
setup_endpoint
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def capture(event)
|
|
47
|
+
deliver(event.scrubbed)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def capabilities
|
|
51
|
+
super.merge(
|
|
52
|
+
breadcrumbs: false,
|
|
53
|
+
code_context: true,
|
|
54
|
+
data_scrubbing: true
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def setup_endpoint
|
|
61
|
+
uri = URI.parse(@endpoint)
|
|
62
|
+
@api_endpoint = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
|
63
|
+
@api_path = "/v1/traces"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_payload(event)
|
|
67
|
+
{
|
|
68
|
+
resourceSpans: [ build_resource_spans(event) ]
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_resource_spans(event)
|
|
73
|
+
{
|
|
74
|
+
resource: build_resource(event),
|
|
75
|
+
scopeSpans: [ build_scope_spans(event) ]
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_resource(event)
|
|
80
|
+
attributes = [
|
|
81
|
+
{ key: "service.name", value: { stringValue: @service_name } },
|
|
82
|
+
{ key: "deployment.environment", value: { stringValue: event.environment.presence || @environment } },
|
|
83
|
+
{ key: "telemetry.sdk.name", value: { stringValue: "lapsoss" } },
|
|
84
|
+
{ key: "telemetry.sdk.version", value: { stringValue: Lapsoss::VERSION } },
|
|
85
|
+
{ key: "telemetry.sdk.language", value: { stringValue: "ruby" } }
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
# Add user context as resource attributes if available
|
|
89
|
+
if event.user_context.present?
|
|
90
|
+
event.user_context.each do |key, value|
|
|
91
|
+
attributes << { key: "user.#{key}", value: attribute_value(value) }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
{ attributes: attributes }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_scope_spans(event)
|
|
99
|
+
{
|
|
100
|
+
scope: {
|
|
101
|
+
name: "lapsoss",
|
|
102
|
+
version: Lapsoss::VERSION
|
|
103
|
+
},
|
|
104
|
+
spans: [ build_span(event) ]
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_span(event)
|
|
109
|
+
now = timestamp_nanos(event.timestamp)
|
|
110
|
+
span_name = event.type == :exception ? event.exception_type : "message"
|
|
111
|
+
|
|
112
|
+
span = {
|
|
113
|
+
traceId: generate_trace_id,
|
|
114
|
+
spanId: generate_span_id,
|
|
115
|
+
name: span_name,
|
|
116
|
+
kind: SPAN_KIND_INTERNAL,
|
|
117
|
+
startTimeUnixNano: now.to_s,
|
|
118
|
+
endTimeUnixNano: now.to_s,
|
|
119
|
+
status: build_status(event),
|
|
120
|
+
attributes: build_span_attributes(event)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Add exception event for exception types
|
|
124
|
+
if event.type == :exception
|
|
125
|
+
span[:events] = [ build_exception_event(event) ]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
span
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def build_status(event)
|
|
132
|
+
if event.type == :exception || event.level == :error || event.level == :fatal
|
|
133
|
+
{ code: STATUS_CODE_ERROR, message: event.exception_message || event.message || "Error" }
|
|
134
|
+
else
|
|
135
|
+
{ code: STATUS_CODE_OK }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def build_span_attributes(event)
|
|
140
|
+
attributes = []
|
|
141
|
+
|
|
142
|
+
# Add tags
|
|
143
|
+
event.tags&.each do |key, value|
|
|
144
|
+
attributes << { key: key.to_s, value: attribute_value(value) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Add extra data
|
|
148
|
+
event.extra&.each do |key, value|
|
|
149
|
+
attributes << { key: "extra.#{key}", value: attribute_value(value) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Add request context
|
|
153
|
+
if event.request_context.present?
|
|
154
|
+
event.request_context.each do |key, value|
|
|
155
|
+
attributes << { key: "http.#{key}", value: attribute_value(value) }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Add transaction name
|
|
160
|
+
if event.transaction.present?
|
|
161
|
+
attributes << { key: "transaction.name", value: { stringValue: event.transaction } }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Add fingerprint
|
|
165
|
+
if event.fingerprint.present?
|
|
166
|
+
attributes << { key: "error.fingerprint", value: { stringValue: event.fingerprint } }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Add message for message events
|
|
170
|
+
if event.type == :message && event.message.present?
|
|
171
|
+
attributes << { key: "message", value: { stringValue: event.message } }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
attributes
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def build_exception_event(event)
|
|
178
|
+
attributes = [
|
|
179
|
+
{ key: "exception.type", value: { stringValue: event.exception_type } },
|
|
180
|
+
{ key: "exception.message", value: { stringValue: event.exception_message } }
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
# Add stacktrace
|
|
184
|
+
if event.has_backtrace?
|
|
185
|
+
attributes << {
|
|
186
|
+
key: "exception.stacktrace",
|
|
187
|
+
value: { stringValue: build_stacktrace_string(event) }
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
{
|
|
192
|
+
name: "exception",
|
|
193
|
+
timeUnixNano: timestamp_nanos(event.timestamp).to_s,
|
|
194
|
+
attributes: attributes
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Convert Ruby value to OTLP attribute value
|
|
199
|
+
def attribute_value(value)
|
|
200
|
+
case value
|
|
201
|
+
when String
|
|
202
|
+
{ stringValue: value }
|
|
203
|
+
when Integer
|
|
204
|
+
{ intValue: value.to_s }
|
|
205
|
+
when Float
|
|
206
|
+
{ doubleValue: value }
|
|
207
|
+
when TrueClass, FalseClass
|
|
208
|
+
{ boolValue: value }
|
|
209
|
+
when Array
|
|
210
|
+
{ arrayValue: { values: value.map { |v| attribute_value(v) } } }
|
|
211
|
+
else
|
|
212
|
+
{ stringValue: value.to_s }
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def serialize_payload(payload)
|
|
217
|
+
json = ActiveSupport::JSON.encode(payload)
|
|
218
|
+
|
|
219
|
+
if json.bytesize >= compress_threshold
|
|
220
|
+
[ ActiveSupport::Gzip.compress(json), true ]
|
|
221
|
+
else
|
|
222
|
+
[ json, false ]
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def adapter_specific_headers
|
|
227
|
+
@headers.dup
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -11,11 +11,13 @@ module Lapsoss
|
|
|
11
11
|
include Concerns::HttpDelivery
|
|
12
12
|
|
|
13
13
|
self.level_mapping_type = :rollbar
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
DEFAULT_API_ENDPOINT = "https://api.rollbar.com"
|
|
15
|
+
DEFAULT_API_PATH = "/api/1/item/"
|
|
16
16
|
|
|
17
17
|
def initialize(name, settings = {})
|
|
18
18
|
super
|
|
19
|
+
@api_endpoint = DEFAULT_API_ENDPOINT
|
|
20
|
+
@api_path = DEFAULT_API_PATH
|
|
19
21
|
@access_token = settings[:access_token].presence || ENV["ROLLBAR_ACCESS_TOKEN"]
|
|
20
22
|
|
|
21
23
|
if @access_token.blank?
|
|
@@ -47,8 +47,8 @@ module Lapsoss
|
|
|
47
47
|
|
|
48
48
|
def setup_endpoint
|
|
49
49
|
uri = URI.parse(@settings[:dsn])
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
@api_endpoint = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
|
51
|
+
@api_path = build_api_path(uri)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def build_api_path(uri)
|
|
@@ -53,8 +53,8 @@ module Lapsoss
|
|
|
53
53
|
debug_log "[TELEBUGS ENDPOINT] Setting endpoint: #{endpoint}"
|
|
54
54
|
debug_log "[TELEBUGS ENDPOINT] Setting API path: #{api_path}"
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
@api_endpoint = endpoint
|
|
57
|
+
@api_path = api_path
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
public
|
|
@@ -63,8 +63,8 @@ module Lapsoss
|
|
|
63
63
|
def capture(event)
|
|
64
64
|
debug_log "[TELEBUGS DEBUG] Capture called for event: #{event.type}"
|
|
65
65
|
debug_log "[TELEBUGS DEBUG] DSN configured: #{@dsn.inspect}"
|
|
66
|
-
debug_log "[TELEBUGS DEBUG] Endpoint: #{
|
|
67
|
-
debug_log "[TELEBUGS DEBUG] API Path: #{
|
|
66
|
+
debug_log "[TELEBUGS DEBUG] Endpoint: #{@api_endpoint}"
|
|
67
|
+
debug_log "[TELEBUGS DEBUG] API Path: #{@api_path}"
|
|
68
68
|
|
|
69
69
|
result = super(event)
|
|
70
70
|
debug_log "[TELEBUGS DEBUG] Event sent successfully, response: #{result.inspect}"
|
data/lib/lapsoss/client.rb
CHANGED
|
@@ -10,15 +10,16 @@ module Lapsoss
|
|
|
10
10
|
# The Concurrent::FixedThreadPool had issues in Rails development mode
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def capture_exception(exception, **context)
|
|
13
|
+
def capture_exception(exception, level: :error, **context)
|
|
14
14
|
return nil unless @configuration.enabled
|
|
15
15
|
|
|
16
|
+
extra_context = context.delete(:context)
|
|
16
17
|
with_scope(context) do |scope|
|
|
17
18
|
event = Event.build(
|
|
18
19
|
type: :exception,
|
|
19
|
-
level:
|
|
20
|
+
level: level,
|
|
20
21
|
exception: exception,
|
|
21
|
-
context: scope_to_context(scope),
|
|
22
|
+
context: merge_context(scope_to_context(scope), extra_context),
|
|
22
23
|
transaction: scope.transaction_name
|
|
23
24
|
)
|
|
24
25
|
capture_event(event)
|
|
@@ -28,12 +29,13 @@ module Lapsoss
|
|
|
28
29
|
def capture_message(message, level: :info, **context)
|
|
29
30
|
return nil unless @configuration.enabled
|
|
30
31
|
|
|
32
|
+
extra_context = context.delete(:context)
|
|
31
33
|
with_scope(context) do |scope|
|
|
32
34
|
event = Event.build(
|
|
33
35
|
type: :message,
|
|
34
36
|
level: level,
|
|
35
37
|
message: message,
|
|
36
|
-
context: scope_to_context(scope),
|
|
38
|
+
context: merge_context(scope_to_context(scope), extra_context),
|
|
37
39
|
transaction: scope.transaction_name
|
|
38
40
|
)
|
|
39
41
|
capture_event(event)
|
|
@@ -86,6 +88,11 @@ module Lapsoss
|
|
|
86
88
|
return nil unless event
|
|
87
89
|
end
|
|
88
90
|
|
|
91
|
+
if (filter = @configuration.exclusion_filter) && filter.should_exclude?(event)
|
|
92
|
+
@configuration.logger.debug("[LAPSOSS] Event excluded by configured exclusion_filter")
|
|
93
|
+
return nil
|
|
94
|
+
end
|
|
95
|
+
|
|
89
96
|
event = run_before_send(event)
|
|
90
97
|
return nil unless event
|
|
91
98
|
|
|
@@ -121,12 +128,23 @@ module Lapsoss
|
|
|
121
128
|
end
|
|
122
129
|
|
|
123
130
|
def scope_to_context(scope)
|
|
131
|
+
defaults = @configuration.default_context
|
|
124
132
|
{
|
|
125
|
-
tags: scope.tags,
|
|
126
|
-
user: scope.user,
|
|
127
|
-
extra: scope.extra,
|
|
133
|
+
tags: (defaults[:tags] || {}).merge(scope.tags),
|
|
134
|
+
user: (defaults[:user] || {}).merge(scope.user),
|
|
135
|
+
extra: (defaults[:extra] || {}).merge(scope.extra),
|
|
128
136
|
breadcrumbs: scope.breadcrumbs
|
|
129
|
-
}
|
|
137
|
+
}.tap do |ctx|
|
|
138
|
+
ctx[:environment] ||= @configuration.environment if @configuration.environment
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def merge_context(scope_context, extra_context)
|
|
143
|
+
return scope_context unless extra_context
|
|
144
|
+
|
|
145
|
+
merged = scope_context.dup
|
|
146
|
+
merged[:context] = (scope_context[:context] || {}).merge(extra_context)
|
|
147
|
+
merged
|
|
130
148
|
end
|
|
131
149
|
|
|
132
150
|
def handle_capture_error(error)
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "logger"
|
|
4
|
-
require "active_support/configurable"
|
|
5
4
|
|
|
6
5
|
module Lapsoss
|
|
7
6
|
class Configuration
|
|
8
7
|
include Validators
|
|
9
|
-
include ActiveSupport::Configurable
|
|
10
8
|
|
|
11
9
|
attr_accessor :async, :logger, :enabled, :release, :debug,
|
|
12
10
|
:scrub_fields, :scrub_all, :whitelist_fields, :randomize_scrub_length,
|
|
@@ -15,7 +13,8 @@ module Lapsoss
|
|
|
15
13
|
:backtrace_context_lines, :backtrace_in_app_patterns, :backtrace_exclude_patterns,
|
|
16
14
|
:backtrace_strip_load_path, :backtrace_max_frames, :backtrace_enable_code_context,
|
|
17
15
|
:enable_pipeline, :pipeline_builder, :sampling_strategy,
|
|
18
|
-
:skip_rails_cache_errors, :force_sync_http, :capture_request_context
|
|
16
|
+
:skip_rails_cache_errors, :force_sync_http, :capture_request_context,
|
|
17
|
+
:exclusion_filter
|
|
19
18
|
attr_reader :fingerprint_callback, :environment, :before_send, :sample_rate, :error_handler, :transport_timeout,
|
|
20
19
|
:transport_max_retries, :transport_initial_backoff, :transport_max_backoff, :transport_backoff_multiplier, :transport_ssl_verify, :default_context, :adapter_configs
|
|
21
20
|
|
|
@@ -67,6 +66,8 @@ module Lapsoss
|
|
|
67
66
|
@force_sync_http = false
|
|
68
67
|
# Capture request context in middleware
|
|
69
68
|
@capture_request_context = true
|
|
69
|
+
# Exclusion filter
|
|
70
|
+
@exclusion_filter = nil
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
# Register a named adapter configuration
|
|
@@ -116,6 +117,35 @@ module Lapsoss
|
|
|
116
117
|
register_adapter(name, :logger, **settings)
|
|
117
118
|
end
|
|
118
119
|
|
|
120
|
+
# Convenience method for OpenObserve
|
|
121
|
+
def use_openobserve(name: :openobserve, **settings)
|
|
122
|
+
register_adapter(name, :openobserve, **settings)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Convenience method for OTLP (OpenTelemetry Protocol)
|
|
126
|
+
# Works with SigNoz, Jaeger, Tempo, Honeycomb, etc.
|
|
127
|
+
def use_otlp(name: :otlp, **settings)
|
|
128
|
+
register_adapter(name, :otlp, **settings)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Convenience method for SigNoz (OTLP-compatible)
|
|
132
|
+
def use_signoz(name: :signoz, **settings)
|
|
133
|
+
settings[:endpoint] ||= "http://localhost:4318"
|
|
134
|
+
register_adapter(name, :otlp, **settings)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Convenience method for Jaeger (OTLP-compatible)
|
|
138
|
+
def use_jaeger(name: :jaeger, **settings)
|
|
139
|
+
settings[:endpoint] ||= "http://localhost:4318"
|
|
140
|
+
register_adapter(name, :otlp, **settings)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Convenience method for Grafana Tempo (OTLP-compatible)
|
|
144
|
+
def use_tempo(name: :tempo, **settings)
|
|
145
|
+
settings[:endpoint] ||= "http://localhost:4318"
|
|
146
|
+
register_adapter(name, :otlp, **settings)
|
|
147
|
+
end
|
|
148
|
+
|
|
119
149
|
# Apply configuration by registering all adapters
|
|
120
150
|
def apply!
|
|
121
151
|
Registry.instance.clear!
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lapsoss
|
|
4
|
+
module Middleware
|
|
5
|
+
# Drops events based on sampling strategy or rate.
|
|
6
|
+
class SampleFilter < Base
|
|
7
|
+
def initialize(app, sample_rate: 1.0, sample_callback: nil, sampler: nil)
|
|
8
|
+
super(app)
|
|
9
|
+
@sampler =
|
|
10
|
+
sampler ||
|
|
11
|
+
sample_callback ||
|
|
12
|
+
Sampling::UniformSampler.new(sample_rate)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(event, hint = {})
|
|
16
|
+
return nil unless sample?(event, hint)
|
|
17
|
+
|
|
18
|
+
@app.call(event, hint)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def sample?(event, hint)
|
|
24
|
+
if @sampler.respond_to?(:sample?)
|
|
25
|
+
@sampler.sample?(event, hint)
|
|
26
|
+
else
|
|
27
|
+
@sampler.call(event, hint)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lapsoss
|
|
4
|
+
module Middleware
|
|
5
|
+
# Adds user info to the event context using a callable provider.
|
|
6
|
+
class UserContextEnhancer < Base
|
|
7
|
+
def initialize(app, user_provider:, privacy_mode: false)
|
|
8
|
+
super(app)
|
|
9
|
+
@user_provider = user_provider
|
|
10
|
+
@privacy_mode = privacy_mode
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(event, hint = {})
|
|
14
|
+
user_data = fetch_user(event, hint)
|
|
15
|
+
return @app.call(event, hint) unless user_data
|
|
16
|
+
|
|
17
|
+
merged_user = (event.context[:user] || {}).merge(user_data)
|
|
18
|
+
merged_user = sanitize_for_privacy(merged_user) if @privacy_mode
|
|
19
|
+
|
|
20
|
+
updated_context = event.context.merge(user: merged_user)
|
|
21
|
+
@app.call(event.with(context: updated_context), hint)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def fetch_user(event, hint)
|
|
27
|
+
return nil unless @user_provider
|
|
28
|
+
|
|
29
|
+
if @user_provider.respond_to?(:call)
|
|
30
|
+
@user_provider.call(event, hint)
|
|
31
|
+
elsif @user_provider.is_a?(Hash)
|
|
32
|
+
@user_provider
|
|
33
|
+
end
|
|
34
|
+
rescue StandardError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def sanitize_for_privacy(user_hash)
|
|
39
|
+
allowed_keys = %i[id uuid user_id]
|
|
40
|
+
user_hash.slice(*allowed_keys)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/lapsoss/router.rb
CHANGED
|
@@ -29,8 +29,9 @@ module Lapsoss
|
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
# Call error handler if configured
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
handled = error.instance_variable_defined?(:@lapsoss_error_handled) &&
|
|
33
|
+
error.instance_variable_get(:@lapsoss_error_handled)
|
|
34
|
+
Lapsoss.call_error_handler(adapter: adapter, event: event, error: error) unless handled
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
end
|
data/lib/lapsoss/scrubber.rb
CHANGED
|
@@ -10,7 +10,13 @@ module Lapsoss
|
|
|
10
10
|
passw email secret token _key crypt salt certificate otp ssn cvv cvc
|
|
11
11
|
].freeze
|
|
12
12
|
|
|
13
|
+
MASK = "[FILTERED]"
|
|
14
|
+
|
|
13
15
|
def initialize(config = {})
|
|
16
|
+
@scrub_all = !!config[:scrub_all]
|
|
17
|
+
@whitelist_fields = Array(config[:whitelist_fields]).map(&:to_s)
|
|
18
|
+
@randomize_scrub_length = !!config[:randomize_scrub_length]
|
|
19
|
+
|
|
14
20
|
# Combine: Rails filter parameters + custom fields (if provided)
|
|
15
21
|
base_params = if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
16
22
|
Rails.application.config.filter_parameters.presence || DEFAULT_SCRUB_FIELDS
|
|
@@ -24,12 +30,94 @@ module Lapsoss
|
|
|
24
30
|
base_params
|
|
25
31
|
end
|
|
26
32
|
|
|
27
|
-
@filter = ActiveSupport::ParameterFilter.new(filter_params)
|
|
33
|
+
@filter = ActiveSupport::ParameterFilter.new(filter_params, mask: MASK)
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
def scrub(data)
|
|
31
37
|
return data if data.nil?
|
|
32
|
-
|
|
38
|
+
|
|
39
|
+
if @scrub_all
|
|
40
|
+
deep_scrub_all(data)
|
|
41
|
+
else
|
|
42
|
+
filtered = @filter.filter(data)
|
|
43
|
+
filtered = restore_whitelisted_values(data, filtered) if @whitelist_fields.any?
|
|
44
|
+
@randomize_scrub_length ? randomize_masks(filtered) : filtered
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def deep_scrub_all(value)
|
|
51
|
+
case value
|
|
52
|
+
when Hash
|
|
53
|
+
value.each_with_object(value.class.new) do |(key, val), result|
|
|
54
|
+
if whitelisted?(key)
|
|
55
|
+
result[key] = val
|
|
56
|
+
else
|
|
57
|
+
result[key] = structure_preserving_mask(val)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
when Array
|
|
61
|
+
value.map { |item| deep_scrub_all(item) }
|
|
62
|
+
else
|
|
63
|
+
mask_value(value)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def structure_preserving_mask(value)
|
|
68
|
+
case value
|
|
69
|
+
when Hash, Array
|
|
70
|
+
deep_scrub_all(value)
|
|
71
|
+
else
|
|
72
|
+
mask_value(value)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def mask_value(value)
|
|
77
|
+
@randomize_scrub_length ? random_mask(value) : MASK
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def random_mask(value)
|
|
81
|
+
length = [ [ value.to_s.length, 3 ].max, 32 ].min
|
|
82
|
+
"*" * length
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def whitelisted?(key)
|
|
86
|
+
@whitelist_fields.include?(key.to_s)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def restore_whitelisted_values(original, filtered)
|
|
90
|
+
case filtered
|
|
91
|
+
when Hash
|
|
92
|
+
filtered.each_with_object(filtered.class.new) do |(key, val), result|
|
|
93
|
+
if whitelisted?(key)
|
|
94
|
+
result[key] = original.is_a?(Hash) ? original[key] : original
|
|
95
|
+
else
|
|
96
|
+
next_original = original.is_a?(Hash) ? original[key] : nil
|
|
97
|
+
result[key] = restore_whitelisted_values(next_original, val)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
when Array
|
|
101
|
+
filtered.each_with_index.map do |val, idx|
|
|
102
|
+
next_original = original.is_a?(Array) ? original[idx] : nil
|
|
103
|
+
restore_whitelisted_values(next_original, val)
|
|
104
|
+
end
|
|
105
|
+
else
|
|
106
|
+
filtered
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def randomize_masks(value)
|
|
111
|
+
case value
|
|
112
|
+
when Hash
|
|
113
|
+
value.transform_values { |v| randomize_masks(v) }
|
|
114
|
+
when Array
|
|
115
|
+
value.map { |v| randomize_masks(v) }
|
|
116
|
+
when String
|
|
117
|
+
value == MASK ? random_mask(value) : value
|
|
118
|
+
else
|
|
119
|
+
value
|
|
120
|
+
end
|
|
33
121
|
end
|
|
34
122
|
end
|
|
35
123
|
end
|
data/lib/lapsoss/version.rb
CHANGED
data/lib/lapsoss.rb
CHANGED
|
@@ -83,6 +83,24 @@ module Lapsoss
|
|
|
83
83
|
client.flush(timeout: timeout)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
def call_error_handler(adapter:, event:, error:)
|
|
87
|
+
handler = configuration.error_handler
|
|
88
|
+
return unless handler
|
|
89
|
+
|
|
90
|
+
case handler.arity
|
|
91
|
+
when 3
|
|
92
|
+
handler.call(adapter, event, error)
|
|
93
|
+
when 2
|
|
94
|
+
handler.call(event, error)
|
|
95
|
+
when 1, 0, -1
|
|
96
|
+
handler.call(error)
|
|
97
|
+
else
|
|
98
|
+
handler.call(adapter, event, error)
|
|
99
|
+
end
|
|
100
|
+
rescue => handler_error
|
|
101
|
+
configuration.logger&.error("[LAPSOSS] Error handler failed: #{handler_error.message}")
|
|
102
|
+
end
|
|
103
|
+
|
|
86
104
|
delegate :shutdown, to: :client
|
|
87
105
|
end
|
|
88
106
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lapsoss
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -221,8 +221,12 @@ files:
|
|
|
221
221
|
- lib/lapsoss/adapters/concerns/envelope_builder.rb
|
|
222
222
|
- lib/lapsoss/adapters/concerns/http_delivery.rb
|
|
223
223
|
- lib/lapsoss/adapters/concerns/level_mapping.rb
|
|
224
|
+
- lib/lapsoss/adapters/concerns/stacktrace_builder.rb
|
|
225
|
+
- lib/lapsoss/adapters/concerns/trace_context.rb
|
|
224
226
|
- lib/lapsoss/adapters/insight_hub_adapter.rb
|
|
225
227
|
- lib/lapsoss/adapters/logger_adapter.rb
|
|
228
|
+
- lib/lapsoss/adapters/openobserve_adapter.rb
|
|
229
|
+
- lib/lapsoss/adapters/otlp_adapter.rb
|
|
226
230
|
- lib/lapsoss/adapters/rollbar_adapter.rb
|
|
227
231
|
- lib/lapsoss/adapters/sentry_adapter.rb
|
|
228
232
|
- lib/lapsoss/adapters/telebugs_adapter.rb
|
|
@@ -247,6 +251,8 @@ files:
|
|
|
247
251
|
- lib/lapsoss/middleware/metrics_collector.rb
|
|
248
252
|
- lib/lapsoss/middleware/rate_limiter.rb
|
|
249
253
|
- lib/lapsoss/middleware/release_tracker.rb
|
|
254
|
+
- lib/lapsoss/middleware/sample_filter.rb
|
|
255
|
+
- lib/lapsoss/middleware/user_context_enhancer.rb
|
|
250
256
|
- lib/lapsoss/pipeline.rb
|
|
251
257
|
- lib/lapsoss/pipeline_builder.rb
|
|
252
258
|
- lib/lapsoss/rails_controller_context.rb
|