lapsoss 0.4.10 → 1.0.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 +4 -4
- data/README.md +125 -3
- 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 +31 -8
- data/lib/lapsoss/configuration.rb +36 -1
- data/lib/lapsoss/current.rb +1 -0
- data/lib/lapsoss/middleware/sample_filter.rb +32 -0
- data/lib/lapsoss/middleware/user_context_enhancer.rb +44 -0
- data/lib/lapsoss/rails_event_subscriber.rb +41 -0
- data/lib/lapsoss/railtie.rb +13 -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 +32 -0
- metadata +11 -4
|
@@ -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)
|
|
@@ -78,6 +80,11 @@ module Lapsoss
|
|
|
78
80
|
private
|
|
79
81
|
|
|
80
82
|
def capture_event(event)
|
|
83
|
+
if Current.silenced
|
|
84
|
+
@configuration.logger.debug("[LAPSOSS] Event dropped: capture silenced via Lapsoss.silence")
|
|
85
|
+
return nil
|
|
86
|
+
end
|
|
87
|
+
|
|
81
88
|
@configuration.logger.debug("[LAPSOSS] capture_event called, async: #{@configuration.async}, executor: #{@executor.inspect}")
|
|
82
89
|
|
|
83
90
|
# Apply pipeline processing if enabled
|
|
@@ -86,6 +93,11 @@ module Lapsoss
|
|
|
86
93
|
return nil unless event
|
|
87
94
|
end
|
|
88
95
|
|
|
96
|
+
if (filter = @configuration.exclusion_filter) && filter.should_exclude?(event)
|
|
97
|
+
@configuration.logger.debug("[LAPSOSS] Event excluded by configured exclusion_filter")
|
|
98
|
+
return nil
|
|
99
|
+
end
|
|
100
|
+
|
|
89
101
|
event = run_before_send(event)
|
|
90
102
|
return nil unless event
|
|
91
103
|
|
|
@@ -121,12 +133,23 @@ module Lapsoss
|
|
|
121
133
|
end
|
|
122
134
|
|
|
123
135
|
def scope_to_context(scope)
|
|
136
|
+
defaults = @configuration.default_context
|
|
124
137
|
{
|
|
125
|
-
tags: scope.tags,
|
|
126
|
-
user: scope.user,
|
|
127
|
-
extra: scope.extra,
|
|
138
|
+
tags: (defaults[:tags] || {}).merge(scope.tags),
|
|
139
|
+
user: (defaults[:user] || {}).merge(scope.user),
|
|
140
|
+
extra: (defaults[:extra] || {}).merge(scope.extra),
|
|
128
141
|
breadcrumbs: scope.breadcrumbs
|
|
129
|
-
}
|
|
142
|
+
}.tap do |ctx|
|
|
143
|
+
ctx[:environment] ||= @configuration.environment if @configuration.environment
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def merge_context(scope_context, extra_context)
|
|
148
|
+
return scope_context unless extra_context
|
|
149
|
+
|
|
150
|
+
merged = scope_context.dup
|
|
151
|
+
merged[:context] = (scope_context[:context] || {}).merge(extra_context)
|
|
152
|
+
merged
|
|
130
153
|
end
|
|
131
154
|
|
|
132
155
|
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, :capture_rails_events, :rails_event_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
|
|
|
@@ -61,10 +62,15 @@ module Lapsoss
|
|
|
61
62
|
@sampling_strategy = nil
|
|
62
63
|
# Rails error filtering
|
|
63
64
|
@skip_rails_cache_errors = true
|
|
65
|
+
# Rails.event structured events as breadcrumbs (Rails 8.1+)
|
|
66
|
+
@capture_rails_events = true
|
|
67
|
+
@rails_event_filter = nil
|
|
64
68
|
# HTTP client settings
|
|
65
69
|
@force_sync_http = false
|
|
66
70
|
# Capture request context in middleware
|
|
67
71
|
@capture_request_context = true
|
|
72
|
+
# Exclusion filter
|
|
73
|
+
@exclusion_filter = nil
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
# Register a named adapter configuration
|
|
@@ -114,6 +120,35 @@ module Lapsoss
|
|
|
114
120
|
register_adapter(name, :logger, **settings)
|
|
115
121
|
end
|
|
116
122
|
|
|
123
|
+
# Convenience method for OpenObserve
|
|
124
|
+
def use_openobserve(name: :openobserve, **settings)
|
|
125
|
+
register_adapter(name, :openobserve, **settings)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Convenience method for OTLP (OpenTelemetry Protocol)
|
|
129
|
+
# Works with SigNoz, Jaeger, Tempo, Honeycomb, etc.
|
|
130
|
+
def use_otlp(name: :otlp, **settings)
|
|
131
|
+
register_adapter(name, :otlp, **settings)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Convenience method for SigNoz (OTLP-compatible)
|
|
135
|
+
def use_signoz(name: :signoz, **settings)
|
|
136
|
+
settings[:endpoint] ||= "http://localhost:4318"
|
|
137
|
+
register_adapter(name, :otlp, **settings)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Convenience method for Jaeger (OTLP-compatible)
|
|
141
|
+
def use_jaeger(name: :jaeger, **settings)
|
|
142
|
+
settings[:endpoint] ||= "http://localhost:4318"
|
|
143
|
+
register_adapter(name, :otlp, **settings)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Convenience method for Grafana Tempo (OTLP-compatible)
|
|
147
|
+
def use_tempo(name: :tempo, **settings)
|
|
148
|
+
settings[:endpoint] ||= "http://localhost:4318"
|
|
149
|
+
register_adapter(name, :otlp, **settings)
|
|
150
|
+
end
|
|
151
|
+
|
|
117
152
|
# Apply configuration by registering all adapters
|
|
118
153
|
def apply!
|
|
119
154
|
Registry.instance.clear!
|
data/lib/lapsoss/current.rb
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lapsoss
|
|
4
|
+
# Subscribes to Rails' structured event reporter (Rails.event, Rails 8.1+)
|
|
5
|
+
# and records emitted events as Lapsoss breadcrumbs, giving error reports an
|
|
6
|
+
# automatic activity trail without any monkey-patching.
|
|
7
|
+
class RailsEventSubscriber
|
|
8
|
+
def emit(event)
|
|
9
|
+
return unless Lapsoss.client
|
|
10
|
+
|
|
11
|
+
Lapsoss.add_breadcrumb(event[:name], type: :event, **breadcrumb_metadata(event))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def breadcrumb_metadata(event)
|
|
17
|
+
metadata = {}
|
|
18
|
+
payload = serialize_payload(event[:payload])
|
|
19
|
+
metadata[:payload] = payload if payload
|
|
20
|
+
metadata[:tags] = event[:tags] if event[:tags].present?
|
|
21
|
+
metadata[:context] = event[:context] if event[:context].present?
|
|
22
|
+
|
|
23
|
+
if (location = event[:source_location])
|
|
24
|
+
metadata[:source] = "#{location[:filepath]}:#{location[:lineno]}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
metadata
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Payloads are either hashes or arbitrary event objects, which Rails passes
|
|
31
|
+
# through as-is and expects subscribers to serialize.
|
|
32
|
+
def serialize_payload(payload)
|
|
33
|
+
case payload
|
|
34
|
+
when nil, Hash
|
|
35
|
+
payload
|
|
36
|
+
else
|
|
37
|
+
payload.respond_to?(:serialize) ? payload.serialize : payload.inspect
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/lapsoss/railtie.rb
CHANGED
|
@@ -39,6 +39,19 @@ module Lapsoss
|
|
|
39
39
|
Rails.error.subscribe(Lapsoss::RailsErrorSubscriber.new)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
initializer "lapsoss.rails_event_subscriber" do
|
|
43
|
+
Rails.event.subscribe(Lapsoss::RailsEventSubscriber.new) do |event|
|
|
44
|
+
config = Lapsoss.configuration
|
|
45
|
+
if !config.capture_rails_events
|
|
46
|
+
false
|
|
47
|
+
elsif (filter = config.rails_event_filter)
|
|
48
|
+
!!filter.call(event)
|
|
49
|
+
else
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
42
55
|
initializer "lapsoss.controller_transaction" do
|
|
43
56
|
ActiveSupport.on_load(:action_controller) do
|
|
44
57
|
require "lapsoss/rails_controller_transaction"
|
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
|