lapsoss 0.4.10 → 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 +75 -0
- 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 -1
- 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
|
@@ -220,6 +220,8 @@ All adapters are pure Ruby implementations with no external SDK dependencies:
|
|
|
220
220
|
- **AppSignal** - Error tracking and deploy markers
|
|
221
221
|
- **Insight Hub** (formerly Bugsnag) - Error tracking with breadcrumbs
|
|
222
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.)
|
|
223
225
|
|
|
224
226
|
## Configuration
|
|
225
227
|
|
|
@@ -254,6 +256,39 @@ Lapsoss.configure do |config|
|
|
|
254
256
|
end
|
|
255
257
|
```
|
|
256
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
|
+
|
|
257
292
|
### Advanced Configuration
|
|
258
293
|
|
|
259
294
|
```ruby
|
|
@@ -276,6 +311,22 @@ Lapsoss.configure do |config|
|
|
|
276
311
|
end
|
|
277
312
|
```
|
|
278
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
|
+
|
|
279
330
|
### Filtering Errors
|
|
280
331
|
|
|
281
332
|
You decide what errors to track. Lapsoss doesn't make assumptions:
|
|
@@ -351,6 +402,30 @@ Rails.application.config.filter_parameters += [:password, :token]
|
|
|
351
402
|
# Lapsoss automatically uses these filters - no additional configuration needed!
|
|
352
403
|
```
|
|
353
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
|
+
|
|
354
429
|
### Custom Fingerprinting
|
|
355
430
|
|
|
356
431
|
Control how errors are grouped:
|
|
@@ -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)
|
|
@@ -13,7 +13,8 @@ module Lapsoss
|
|
|
13
13
|
:backtrace_context_lines, :backtrace_in_app_patterns, :backtrace_exclude_patterns,
|
|
14
14
|
:backtrace_strip_load_path, :backtrace_max_frames, :backtrace_enable_code_context,
|
|
15
15
|
:enable_pipeline, :pipeline_builder, :sampling_strategy,
|
|
16
|
-
:skip_rails_cache_errors, :force_sync_http, :capture_request_context
|
|
16
|
+
:skip_rails_cache_errors, :force_sync_http, :capture_request_context,
|
|
17
|
+
:exclusion_filter
|
|
17
18
|
attr_reader :fingerprint_callback, :environment, :before_send, :sample_rate, :error_handler, :transport_timeout,
|
|
18
19
|
:transport_max_retries, :transport_initial_backoff, :transport_max_backoff, :transport_backoff_multiplier, :transport_ssl_verify, :default_context, :adapter_configs
|
|
19
20
|
|
|
@@ -65,6 +66,8 @@ module Lapsoss
|
|
|
65
66
|
@force_sync_http = false
|
|
66
67
|
# Capture request context in middleware
|
|
67
68
|
@capture_request_context = true
|
|
69
|
+
# Exclusion filter
|
|
70
|
+
@exclusion_filter = nil
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
# Register a named adapter configuration
|
|
@@ -114,6 +117,35 @@ module Lapsoss
|
|
|
114
117
|
register_adapter(name, :logger, **settings)
|
|
115
118
|
end
|
|
116
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
|
+
|
|
117
149
|
# Apply configuration by registering all adapters
|
|
118
150
|
def apply!
|
|
119
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
|