lapsoss 0.1.0 → 0.3.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 +153 -733
- data/lib/lapsoss/adapters/appsignal_adapter.rb +7 -8
- data/lib/lapsoss/adapters/base.rb +0 -3
- data/lib/lapsoss/adapters/bugsnag_adapter.rb +12 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +102 -101
- data/lib/lapsoss/adapters/logger_adapter.rb +7 -7
- data/lib/lapsoss/adapters/rollbar_adapter.rb +93 -54
- data/lib/lapsoss/adapters/sentry_adapter.rb +11 -17
- data/lib/lapsoss/backtrace_frame.rb +35 -214
- data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
- data/lib/lapsoss/backtrace_processor.rb +37 -37
- data/lib/lapsoss/client.rb +2 -6
- data/lib/lapsoss/configuration.rb +25 -22
- data/lib/lapsoss/current.rb +9 -1
- data/lib/lapsoss/event.rb +30 -6
- data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
- data/lib/lapsoss/exclusion_configuration.rb +30 -0
- data/lib/lapsoss/exclusion_filter.rb +156 -0
- data/lib/lapsoss/{exclusions.rb → exclusion_presets.rb} +1 -181
- data/lib/lapsoss/fingerprinter.rb +9 -13
- data/lib/lapsoss/http_client.rb +42 -8
- data/lib/lapsoss/merged_scope.rb +63 -0
- data/lib/lapsoss/middleware/base.rb +15 -0
- data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
- data/lib/lapsoss/middleware/event_enricher.rb +19 -0
- data/lib/lapsoss/middleware/event_transformer.rb +19 -0
- data/lib/lapsoss/middleware/exception_filter.rb +43 -0
- data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
- data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
- data/lib/lapsoss/middleware/release_tracker.rb +117 -0
- data/lib/lapsoss/middleware/sample_filter.rb +23 -0
- data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
- data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
- data/lib/lapsoss/middleware.rb +0 -347
- data/lib/lapsoss/pipeline.rb +1 -73
- data/lib/lapsoss/pipeline_builder.rb +69 -0
- data/lib/lapsoss/rails_error_subscriber.rb +42 -0
- data/lib/lapsoss/rails_middleware.rb +78 -0
- data/lib/lapsoss/railtie.rb +22 -50
- data/lib/lapsoss/registry.rb +34 -20
- data/lib/lapsoss/release_providers.rb +110 -0
- data/lib/lapsoss/release_tracker.rb +112 -207
- data/lib/lapsoss/router.rb +3 -5
- data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
- data/lib/lapsoss/sampling/base.rb +11 -0
- data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
- data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
- data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
- data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
- data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
- data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
- data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
- data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
- data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
- data/lib/lapsoss/sampling.rb +0 -326
- data/lib/lapsoss/scope.rb +17 -57
- data/lib/lapsoss/scrubber.rb +16 -18
- data/lib/lapsoss/user_context.rb +18 -198
- data/lib/lapsoss/user_context_integrations.rb +39 -0
- data/lib/lapsoss/user_context_middleware.rb +50 -0
- data/lib/lapsoss/user_context_provider.rb +93 -0
- data/lib/lapsoss/utils.rb +13 -0
- data/lib/lapsoss/validators.rb +14 -27
- data/lib/lapsoss/version.rb +1 -1
- data/lib/lapsoss.rb +12 -25
- metadata +106 -21
@@ -51,7 +51,7 @@ module Lapsoss
|
|
51
51
|
|
52
52
|
# File system patterns
|
53
53
|
{
|
54
|
-
pattern:
|
54
|
+
pattern: %r{Errno::(ENOENT|EACCES).*/tmp/},
|
55
55
|
fingerprint: "tmp-file-error"
|
56
56
|
},
|
57
57
|
{
|
@@ -121,9 +121,7 @@ module Lapsoss
|
|
121
121
|
parts << event.message if event.message
|
122
122
|
|
123
123
|
# Include first few backtrace lines for context
|
124
|
-
if event.exception&.backtrace
|
125
|
-
parts.concat(event.exception.backtrace.first(3))
|
126
|
-
end
|
124
|
+
parts.concat(event.exception.backtrace.first(3)) if event.exception&.backtrace
|
127
125
|
|
128
126
|
parts.compact.join(" ")
|
129
127
|
end
|
@@ -148,9 +146,7 @@ module Lapsoss
|
|
148
146
|
end
|
149
147
|
|
150
148
|
# Include environment if configured
|
151
|
-
if @include_environment && event.environment
|
152
|
-
components << event.environment
|
153
|
-
end
|
149
|
+
components << event.environment if @include_environment && event.environment
|
154
150
|
|
155
151
|
# Generate hash from components
|
156
152
|
content = components.compact.join("|")
|
@@ -194,18 +190,18 @@ module Lapsoss
|
|
194
190
|
|
195
191
|
# Find first non-gem, non-framework line
|
196
192
|
app_line = backtrace.find do |line|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
193
|
+
line.exclude?("/gems/") &&
|
194
|
+
line.exclude?("/ruby/") &&
|
195
|
+
line.exclude?("(eval)") &&
|
196
|
+
!line.start_with?("[")
|
201
197
|
end
|
202
198
|
|
203
199
|
line_to_use = app_line || backtrace.first
|
204
200
|
|
205
201
|
if @normalize_paths
|
206
202
|
# Extract just filename:line_number
|
207
|
-
if line_to_use
|
208
|
-
"#{
|
203
|
+
if line_to_use =~ %r{([^/]+):(\d+)}
|
204
|
+
"#{::Regexp.last_match(1)}:#{::Regexp.last_match(2)}"
|
209
205
|
else
|
210
206
|
line_to_use
|
211
207
|
end
|
data/lib/lapsoss/http_client.rb
CHANGED
@@ -8,7 +8,7 @@ require "zlib"
|
|
8
8
|
module Lapsoss
|
9
9
|
# HTTP client wrapper using Faraday with retry logic
|
10
10
|
class HttpClient
|
11
|
-
USER_AGENT = "lapsoss/#{Lapsoss::VERSION}"
|
11
|
+
USER_AGENT = "lapsoss/#{Lapsoss::VERSION}".freeze
|
12
12
|
|
13
13
|
def initialize(base_url, config = {})
|
14
14
|
@base_url = base_url
|
@@ -53,26 +53,60 @@ module Lapsoss
|
|
53
53
|
conn.options.open_timeout = @config[:timeout] || 5
|
54
54
|
|
55
55
|
# Configure SSL
|
56
|
-
if @config.key?(:ssl_verify)
|
57
|
-
conn.ssl.verify = @config[:ssl_verify]
|
58
|
-
end
|
56
|
+
conn.ssl.verify = @config[:ssl_verify] if @config.key?(:ssl_verify)
|
59
57
|
|
60
58
|
# Set user agent
|
61
59
|
conn.headers["User-Agent"] = USER_AGENT
|
62
60
|
|
63
|
-
#
|
64
|
-
conn.adapter
|
61
|
+
# Auto-detect and use appropriate adapter
|
62
|
+
conn.adapter detect_optimal_adapter
|
65
63
|
end
|
66
64
|
end
|
67
65
|
|
66
|
+
def detect_optimal_adapter
|
67
|
+
if fiber_scheduler_active? && async_adapter_available? && !force_sync_mode?
|
68
|
+
log_adapter_selection(:async)
|
69
|
+
:async_http
|
70
|
+
else
|
71
|
+
log_adapter_selection(:sync)
|
72
|
+
Faraday.default_adapter
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def fiber_scheduler_active?
|
77
|
+
Fiber.current_scheduler != nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def async_adapter_available?
|
81
|
+
require "async/http/faraday"
|
82
|
+
true
|
83
|
+
rescue LoadError
|
84
|
+
false
|
85
|
+
end
|
86
|
+
|
87
|
+
def force_sync_mode?
|
88
|
+
Lapsoss.configuration.force_sync_http
|
89
|
+
end
|
90
|
+
|
91
|
+
def log_adapter_selection(adapter_type)
|
92
|
+
return unless Lapsoss.configuration.debug?
|
93
|
+
|
94
|
+
Lapsoss.configuration.logger&.debug(
|
95
|
+
"[Lapsoss::HttpClient] Using #{adapter_type} HTTP adapter " \
|
96
|
+
"(fiber_scheduler: #{fiber_scheduler_active?}, " \
|
97
|
+
"async_available: #{async_adapter_available?}, " \
|
98
|
+
"force_sync: #{force_sync_mode?})"
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
68
102
|
def retry_options
|
69
103
|
{
|
70
104
|
max: @config[:max_retries] || 3,
|
71
105
|
interval: @config[:initial_backoff] || 1.0,
|
72
106
|
max_interval: @config[:max_backoff] || 64.0,
|
73
107
|
backoff_factor: @config[:backoff_multiplier] || 2.0,
|
74
|
-
retry_statuses: [408, 429, 500, 502, 503, 504],
|
75
|
-
methods: [:post]
|
108
|
+
retry_statuses: [ 408, 429, 500, 502, 503, 504 ],
|
109
|
+
methods: [ :post ]
|
76
110
|
}
|
77
111
|
end
|
78
112
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
# Performance-optimized scope that provides a merged view without cloning
|
5
|
+
class MergedScope
|
6
|
+
def initialize(scope_stack, base_scope)
|
7
|
+
@scope_stack = scope_stack
|
8
|
+
@base_scope = base_scope || Scope.new
|
9
|
+
@own_breadcrumbs = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def tags
|
13
|
+
@tags ||= merge_hash_contexts(:tags)
|
14
|
+
end
|
15
|
+
|
16
|
+
def user
|
17
|
+
@user ||= merge_hash_contexts(:user)
|
18
|
+
end
|
19
|
+
|
20
|
+
def extra
|
21
|
+
@extra ||= merge_hash_contexts(:extra)
|
22
|
+
end
|
23
|
+
|
24
|
+
def breadcrumbs
|
25
|
+
@breadcrumbs ||= merge_breadcrumbs
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_breadcrumb(message, type: :default, **metadata)
|
29
|
+
breadcrumb = {
|
30
|
+
message: message,
|
31
|
+
type: type,
|
32
|
+
metadata: metadata,
|
33
|
+
timestamp: Time.now.utc
|
34
|
+
}
|
35
|
+
@own_breadcrumbs << breadcrumb
|
36
|
+
# Keep breadcrumbs to a reasonable limit
|
37
|
+
@own_breadcrumbs.shift if @own_breadcrumbs.length > 20
|
38
|
+
# Clear cached breadcrumbs to force recomputation
|
39
|
+
@breadcrumbs = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def merge_hash_contexts(key)
|
45
|
+
result = @base_scope.send(key).dup
|
46
|
+
@scope_stack.each do |context|
|
47
|
+
result.merge!(context[key] || {})
|
48
|
+
end
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
def merge_breadcrumbs
|
53
|
+
result = @base_scope.breadcrumbs.dup
|
54
|
+
@scope_stack.each do |context|
|
55
|
+
result.concat(context[:breadcrumbs]) if context[:breadcrumbs]
|
56
|
+
end
|
57
|
+
# Add our own breadcrumbs
|
58
|
+
result.concat(@own_breadcrumbs)
|
59
|
+
# Keep breadcrumbs to a reasonable limit
|
60
|
+
result.last(20)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
module Middleware
|
5
|
+
class ConditionalFilter < Base
|
6
|
+
def initialize(app, condition)
|
7
|
+
super(app)
|
8
|
+
@condition = condition
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(event, hint = {})
|
12
|
+
return nil unless @condition.call(event, hint)
|
13
|
+
|
14
|
+
@app.call(event, hint)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
module Middleware
|
5
|
+
class EventEnricher < Base
|
6
|
+
def initialize(app, enrichers: [])
|
7
|
+
super(app)
|
8
|
+
@enrichers = enrichers
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(event, hint = {})
|
12
|
+
@enrichers.each do |enricher|
|
13
|
+
enricher.call(event, hint)
|
14
|
+
end
|
15
|
+
@app.call(event, hint)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
module Middleware
|
5
|
+
class EventTransformer < Base
|
6
|
+
def initialize(app, transformer)
|
7
|
+
super(app)
|
8
|
+
@transformer = transformer
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(event, hint = {})
|
12
|
+
transformed_event = @transformer.call(event, hint)
|
13
|
+
return nil unless transformed_event
|
14
|
+
|
15
|
+
@app.call(transformed_event, hint)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
module Middleware
|
5
|
+
class ExceptionFilter < Base
|
6
|
+
def initialize(app, excluded_exceptions: [], excluded_patterns: [])
|
7
|
+
super(app)
|
8
|
+
@excluded_exceptions = Array(excluded_exceptions)
|
9
|
+
@excluded_patterns = Array(excluded_patterns)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(event, hint = {})
|
13
|
+
return nil if should_exclude?(event)
|
14
|
+
|
15
|
+
@app.call(event, hint)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def should_exclude?(event)
|
21
|
+
return false unless event.exception
|
22
|
+
|
23
|
+
exception_class = event.exception.class
|
24
|
+
exception_message = event.exception.message
|
25
|
+
|
26
|
+
# Check exact class matches
|
27
|
+
return true if @excluded_exceptions.any? { |klass| exception_class <= klass }
|
28
|
+
|
29
|
+
# Check pattern matches
|
30
|
+
@excluded_patterns.any? do |pattern|
|
31
|
+
case pattern
|
32
|
+
when Regexp
|
33
|
+
exception_message&.match?(pattern) || exception_class.name.match?(pattern)
|
34
|
+
when String
|
35
|
+
exception_message&.include?(pattern) || exception_class.name.include?(pattern)
|
36
|
+
else
|
37
|
+
false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
module Middleware
|
5
|
+
class MetricsCollector < Base
|
6
|
+
def initialize(app, collector: nil)
|
7
|
+
super(app)
|
8
|
+
@collector = collector
|
9
|
+
@metrics = {
|
10
|
+
events_processed: 0,
|
11
|
+
events_dropped: 0,
|
12
|
+
events_by_type: Hash.new(0),
|
13
|
+
events_by_level: Hash.new(0)
|
14
|
+
}
|
15
|
+
@mutex = Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(event, hint = {})
|
19
|
+
@mutex.synchronize do
|
20
|
+
@metrics[:events_processed] += 1
|
21
|
+
@metrics[:events_by_type][event.type] += 1
|
22
|
+
@metrics[:events_by_level][event.level] += 1
|
23
|
+
end
|
24
|
+
|
25
|
+
result = @app.call(event, hint)
|
26
|
+
|
27
|
+
if result.nil?
|
28
|
+
@mutex.synchronize do
|
29
|
+
@metrics[:events_dropped] += 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Send to external collector if provided
|
34
|
+
@collector&.call(@metrics.dup, event, hint)
|
35
|
+
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def metrics
|
40
|
+
@mutex.synchronize { @metrics.dup }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
module Middleware
|
5
|
+
class RateLimiter < Base
|
6
|
+
def initialize(app, max_events: 100, time_window: 60)
|
7
|
+
super(app)
|
8
|
+
@max_events = max_events
|
9
|
+
@time_window = time_window
|
10
|
+
@events = []
|
11
|
+
@mutex = Mutex.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(event, hint = {})
|
15
|
+
@mutex.synchronize do
|
16
|
+
now = Time.zone.now
|
17
|
+
# Remove old events outside time window
|
18
|
+
@events.reject! { |timestamp| now - timestamp > @time_window }
|
19
|
+
|
20
|
+
# Check if we're over the limit
|
21
|
+
return nil if @events.length >= @max_events
|
22
|
+
|
23
|
+
# Add current event
|
24
|
+
@events << now
|
25
|
+
end
|
26
|
+
|
27
|
+
@app.call(event, hint)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
module Middleware
|
5
|
+
class ReleaseTracker < Base
|
6
|
+
def initialize(app, release_provider: nil)
|
7
|
+
super(app)
|
8
|
+
@release_provider = release_provider
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(event, hint = {})
|
12
|
+
add_release_info(event, hint)
|
13
|
+
@app.call(event, hint)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def add_release_info(event, hint)
|
19
|
+
release_info = @release_provider&.call(event, hint) || auto_detect_release
|
20
|
+
|
21
|
+
event.context[:release] = release_info if release_info
|
22
|
+
end
|
23
|
+
|
24
|
+
def auto_detect_release
|
25
|
+
release_info = {}
|
26
|
+
|
27
|
+
# Try to detect Git information
|
28
|
+
if git_info = detect_git_info
|
29
|
+
release_info.merge!(git_info)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Try to detect deployment info
|
33
|
+
if deployment_info = detect_deployment_info
|
34
|
+
release_info.merge!(deployment_info)
|
35
|
+
end
|
36
|
+
|
37
|
+
release_info.empty? ? nil : release_info
|
38
|
+
end
|
39
|
+
|
40
|
+
def detect_git_info
|
41
|
+
return nil unless File.exist?(".git")
|
42
|
+
|
43
|
+
begin
|
44
|
+
# Get current commit SHA
|
45
|
+
commit_sha = `git rev-parse HEAD`.strip
|
46
|
+
return nil if commit_sha.empty?
|
47
|
+
|
48
|
+
# Get branch name
|
49
|
+
branch = `git rev-parse --abbrev-ref HEAD`.strip
|
50
|
+
branch = nil if branch.empty? || branch == "HEAD"
|
51
|
+
|
52
|
+
# Get commit timestamp
|
53
|
+
commit_time = `git log -1 --format=%ct`.strip
|
54
|
+
commit_timestamp = commit_time.empty? ? nil : Time.zone.at(commit_time.to_i)
|
55
|
+
|
56
|
+
# Get tag if on a tag
|
57
|
+
tag = `git describe --exact-match --tags HEAD 2>/dev/null`.strip
|
58
|
+
tag = nil if tag.empty?
|
59
|
+
|
60
|
+
{
|
61
|
+
commit_sha: commit_sha,
|
62
|
+
branch: branch,
|
63
|
+
tag: tag,
|
64
|
+
commit_timestamp: commit_timestamp
|
65
|
+
}.compact
|
66
|
+
rescue StandardError
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def detect_deployment_info
|
72
|
+
info = {}
|
73
|
+
|
74
|
+
# Check common deployment environment variables
|
75
|
+
info[:deployment_id] = ENV["DEPLOYMENT_ID"] if ENV["DEPLOYMENT_ID"]
|
76
|
+
info[:build_number] = ENV["BUILD_NUMBER"] if ENV["BUILD_NUMBER"]
|
77
|
+
info[:deployment_time] = parse_deployment_time(ENV["DEPLOYMENT_TIME"]) if ENV["DEPLOYMENT_TIME"]
|
78
|
+
|
79
|
+
# Check Heroku
|
80
|
+
if ENV["HEROKU_APP_NAME"]
|
81
|
+
info[:platform] = "heroku"
|
82
|
+
info[:app_name] = ENV["HEROKU_APP_NAME"]
|
83
|
+
info[:dyno] = ENV.fetch("DYNO", nil)
|
84
|
+
info[:slug_commit] = ENV.fetch("HEROKU_SLUG_COMMIT", nil)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Check AWS
|
88
|
+
if ENV["AWS_EXECUTION_ENV"]
|
89
|
+
info[:platform] = "aws"
|
90
|
+
info[:execution_env] = ENV["AWS_EXECUTION_ENV"]
|
91
|
+
info[:region] = ENV.fetch("AWS_REGION", nil)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Check Docker
|
95
|
+
if ENV["DOCKER_CONTAINER_ID"] || File.exist?("/.dockerenv")
|
96
|
+
info[:platform] = "docker"
|
97
|
+
info[:container_id] = ENV["DOCKER_CONTAINER_ID"]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Check Kubernetes
|
101
|
+
if ENV["KUBERNETES_SERVICE_HOST"]
|
102
|
+
info[:platform] = "kubernetes"
|
103
|
+
info[:namespace] = ENV.fetch("KUBERNETES_NAMESPACE", nil)
|
104
|
+
info[:pod_name] = ENV.fetch("HOSTNAME", nil)
|
105
|
+
end
|
106
|
+
|
107
|
+
info.empty? ? nil : info
|
108
|
+
end
|
109
|
+
|
110
|
+
def parse_deployment_time(time_str)
|
111
|
+
Time.zone.parse(time_str)
|
112
|
+
rescue StandardError
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
module Middleware
|
5
|
+
class SampleFilter < Base
|
6
|
+
def initialize(app, sample_rate: 1.0, sample_callback: nil)
|
7
|
+
super(app)
|
8
|
+
@sample_rate = sample_rate
|
9
|
+
@sample_callback = sample_callback
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(event, hint = {})
|
13
|
+
# Apply custom sampling logic first
|
14
|
+
return nil if @sample_callback && !@sample_callback.call(event, hint)
|
15
|
+
|
16
|
+
# Apply rate-based sampling
|
17
|
+
return nil if (@sample_rate < 1.0) && (rand > @sample_rate)
|
18
|
+
|
19
|
+
@app.call(event, hint)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
module Middleware
|
5
|
+
class SamplingMiddleware < Base
|
6
|
+
def initialize(app, sampler)
|
7
|
+
super(app)
|
8
|
+
@sampler = sampler
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(event, hint = {})
|
12
|
+
return nil unless @sampler.sample?(event, hint)
|
13
|
+
|
14
|
+
@app.call(event, hint)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
module Middleware
|
5
|
+
class UserContextEnhancer < Base
|
6
|
+
def initialize(app, user_provider: nil, privacy_mode: false)
|
7
|
+
super(app)
|
8
|
+
@user_provider = user_provider
|
9
|
+
@privacy_mode = privacy_mode
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(event, hint = {})
|
13
|
+
enhance_user_context(event, hint)
|
14
|
+
@app.call(event, hint)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def enhance_user_context(event, hint)
|
20
|
+
# Get user from provider if available
|
21
|
+
user_data = @user_provider&.call(event, hint) || {}
|
22
|
+
|
23
|
+
# Merge with existing user context
|
24
|
+
existing_user = event.context[:user] || {}
|
25
|
+
enhanced_user = existing_user.merge(user_data)
|
26
|
+
|
27
|
+
# Apply privacy filtering if enabled
|
28
|
+
enhanced_user = apply_privacy_filtering(enhanced_user) if @privacy_mode
|
29
|
+
|
30
|
+
event.context[:user] = enhanced_user unless enhanced_user.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def apply_privacy_filtering(user_data)
|
34
|
+
# Remove sensitive fields in privacy mode
|
35
|
+
sensitive_fields = %i[email phone address ssn credit_card]
|
36
|
+
filtered = user_data.dup
|
37
|
+
|
38
|
+
sensitive_fields.each do |field|
|
39
|
+
filtered[field] = "[FILTERED]" if filtered.key?(field)
|
40
|
+
end
|
41
|
+
|
42
|
+
filtered
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|