lapsoss 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +855 -0
- data/lib/lapsoss/adapters/appsignal_adapter.rb +136 -0
- data/lib/lapsoss/adapters/base.rb +88 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +190 -0
- data/lib/lapsoss/adapters/logger_adapter.rb +67 -0
- data/lib/lapsoss/adapters/rollbar_adapter.rb +157 -0
- data/lib/lapsoss/adapters/sentry_adapter.rb +197 -0
- data/lib/lapsoss/backtrace_frame.rb +258 -0
- data/lib/lapsoss/backtrace_processor.rb +346 -0
- data/lib/lapsoss/client.rb +115 -0
- data/lib/lapsoss/configuration.rb +310 -0
- data/lib/lapsoss/current.rb +9 -0
- data/lib/lapsoss/event.rb +107 -0
- data/lib/lapsoss/exclusions.rb +429 -0
- data/lib/lapsoss/fingerprinter.rb +217 -0
- data/lib/lapsoss/http_client.rb +79 -0
- data/lib/lapsoss/middleware.rb +353 -0
- data/lib/lapsoss/pipeline.rb +131 -0
- data/lib/lapsoss/railtie.rb +72 -0
- data/lib/lapsoss/registry.rb +114 -0
- data/lib/lapsoss/release_tracker.rb +553 -0
- data/lib/lapsoss/router.rb +36 -0
- data/lib/lapsoss/sampling.rb +332 -0
- data/lib/lapsoss/scope.rb +110 -0
- data/lib/lapsoss/scrubber.rb +170 -0
- data/lib/lapsoss/user_context.rb +355 -0
- data/lib/lapsoss/validators.rb +142 -0
- data/lib/lapsoss/version.rb +5 -0
- data/lib/lapsoss.rb +76 -0
- metadata +217 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
require_relative "../http_client"
|
|
6
|
+
|
|
7
|
+
module Lapsoss
|
|
8
|
+
module Adapters
|
|
9
|
+
class AppsignalAdapter < Base
|
|
10
|
+
PUSH_API_URI = "https://push.appsignal.com"
|
|
11
|
+
ERRORS_API_URI = "https://appsignal-endpoint.net"
|
|
12
|
+
JSON_CONTENT_TYPE = "application/json; charset=UTF-8"
|
|
13
|
+
|
|
14
|
+
def initialize(name, settings = {})
|
|
15
|
+
super(name, settings)
|
|
16
|
+
@push_api_key = settings[:push_api_key] || ENV["APPSIGNAL_PUSH_API_KEY"]
|
|
17
|
+
@frontend_api_key = settings[:frontend_api_key] || ENV["APPSIGNAL_FRONTEND_API_KEY"]
|
|
18
|
+
@app_name = settings[:app_name] || ENV["APPSIGNAL_APP_NAME"]
|
|
19
|
+
@environment = Lapsoss.configuration.environment
|
|
20
|
+
|
|
21
|
+
validate_settings!
|
|
22
|
+
|
|
23
|
+
@push_client = create_http_client(PUSH_API_URI) if @push_api_key
|
|
24
|
+
@errors_client = create_http_client(ERRORS_API_URI) if @frontend_api_key
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def capture(event)
|
|
28
|
+
return unless enabled? && @errors_client
|
|
29
|
+
|
|
30
|
+
payload = build_error_payload(event)
|
|
31
|
+
return unless payload
|
|
32
|
+
|
|
33
|
+
path = "/errors?api_key=#{@frontend_api_key}"
|
|
34
|
+
headers = { "Content-Type" => JSON_CONTENT_TYPE }
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
@errors_client.post(path, body: JSON.generate(payload), headers: headers)
|
|
38
|
+
rescue DeliveryError => e
|
|
39
|
+
# Log the error and potentially notify error handler
|
|
40
|
+
Lapsoss.configuration.logger&.error("[Lapsoss::AppsignalAdapter] Failed to deliver event: #{e.message}")
|
|
41
|
+
Lapsoss.configuration.error_handler&.call(e)
|
|
42
|
+
|
|
43
|
+
# Re-raise to let the caller know delivery failed
|
|
44
|
+
raise
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def shutdown
|
|
49
|
+
@push_client&.shutdown
|
|
50
|
+
@errors_client&.shutdown
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def build_error_payload(event)
|
|
57
|
+
case event.type
|
|
58
|
+
when :exception
|
|
59
|
+
build_exception_payload(event)
|
|
60
|
+
when :message
|
|
61
|
+
build_message_payload(event)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def build_exception_payload(event)
|
|
66
|
+
{
|
|
67
|
+
timestamp: event.timestamp.to_i,
|
|
68
|
+
namespace: event.context[:namespace] || "backend",
|
|
69
|
+
error: {
|
|
70
|
+
name: event.exception.class.name,
|
|
71
|
+
message: event.exception.message,
|
|
72
|
+
backtrace: event.exception.backtrace || []
|
|
73
|
+
},
|
|
74
|
+
tags: stringify_hash(event.context[:tags]),
|
|
75
|
+
params: stringify_hash(event.context[:params]),
|
|
76
|
+
environment: build_environment_context(event),
|
|
77
|
+
breadcrumbs: event.context[:breadcrumbs]
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_message_payload(event)
|
|
82
|
+
# AppSignal's Error API expects exception-like structure
|
|
83
|
+
# Instead of creating fake exceptions, we'll structure the message properly
|
|
84
|
+
# but clearly indicate it's a log message, not an exception
|
|
85
|
+
|
|
86
|
+
unless [:error, :fatal, :critical].include?(event.level)
|
|
87
|
+
# Log when messages are dropped due to level filtering
|
|
88
|
+
Lapsoss.configuration.logger&.debug(
|
|
89
|
+
"[Lapsoss::AppsignalAdapter] Dropping message with level '#{event.level}' - " \
|
|
90
|
+
"AppSignal only supports :error, :fatal, and :critical levels for messages"
|
|
91
|
+
)
|
|
92
|
+
return nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
action: event.context[:action] || "log_message",
|
|
97
|
+
path: event.context[:path] || "/",
|
|
98
|
+
exception: {
|
|
99
|
+
# AppSignal requires exception format for messages - this isn't a real exception
|
|
100
|
+
# but rather a way to send structured log messages through their error API
|
|
101
|
+
name: "LogMessage", # Clear indication this is a log message
|
|
102
|
+
message: event.message,
|
|
103
|
+
backtrace: [] # No fake backtrace for log messages
|
|
104
|
+
},
|
|
105
|
+
tags: stringify_hash(event.context[:tags]),
|
|
106
|
+
params: stringify_hash(event.context[:params]),
|
|
107
|
+
environment: build_environment_context(event),
|
|
108
|
+
breadcrumbs: event.context[:breadcrumbs]
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build_environment_context(event)
|
|
113
|
+
{
|
|
114
|
+
"hostname" => Socket.gethostname,
|
|
115
|
+
"app_name" => @app_name,
|
|
116
|
+
"environment" => @environment
|
|
117
|
+
}.merge(stringify_hash(event.context[:environment] || {}))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def stringify_hash(hash)
|
|
121
|
+
(hash || {}).transform_keys(&:to_s).transform_values(&:to_s)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def validate_settings!
|
|
125
|
+
unless @push_api_key || @frontend_api_key
|
|
126
|
+
raise ValidationError, "AppSignal API key is required (either push_api_key or frontend_api_key)"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
validate_api_key!(@push_api_key, "AppSignal push API key", format: :uuid) if @push_api_key
|
|
130
|
+
validate_api_key!(@frontend_api_key, "AppSignal frontend API key", format: :uuid) if @frontend_api_key
|
|
131
|
+
validate_presence!(@app_name, "AppSignal app name") if @app_name
|
|
132
|
+
validate_environment!(@environment, "AppSignal environment") if @environment
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../http_client"
|
|
4
|
+
require_relative "../validators"
|
|
5
|
+
|
|
6
|
+
module Lapsoss
|
|
7
|
+
module Adapters
|
|
8
|
+
class Base
|
|
9
|
+
include Validators
|
|
10
|
+
|
|
11
|
+
attr_reader :name, :settings
|
|
12
|
+
|
|
13
|
+
def initialize(name, settings = {})
|
|
14
|
+
@name = name
|
|
15
|
+
@settings = settings
|
|
16
|
+
@enabled = true
|
|
17
|
+
configure_sdk
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def enabled?
|
|
21
|
+
@enabled
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def enable!
|
|
25
|
+
@enabled = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def disable!
|
|
29
|
+
@enabled = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def capabilities
|
|
33
|
+
{
|
|
34
|
+
errors: true,
|
|
35
|
+
performance: false,
|
|
36
|
+
sessions: false,
|
|
37
|
+
feature_flags: false,
|
|
38
|
+
check_ins: false,
|
|
39
|
+
breadcrumbs: false,
|
|
40
|
+
deployments: false,
|
|
41
|
+
metrics: false,
|
|
42
|
+
profiling: false,
|
|
43
|
+
security: false,
|
|
44
|
+
code_context: false,
|
|
45
|
+
data_scrubbing: false
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def supports?(capability)
|
|
50
|
+
capabilities[capability] == true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def capture(event)
|
|
54
|
+
raise NotImplementedError, "#{self.class.name} must implement #capture"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def flush(timeout: 2)
|
|
58
|
+
# Optional implementation for flushing pending events
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def shutdown
|
|
62
|
+
@enabled = false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def configure_sdk
|
|
68
|
+
# Override in subclass to configure vendor SDK
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_http_client(uri, custom_config = {})
|
|
72
|
+
config = Lapsoss.configuration
|
|
73
|
+
|
|
74
|
+
transport_config = {
|
|
75
|
+
timeout: config.transport_timeout,
|
|
76
|
+
max_retries: config.transport_max_retries,
|
|
77
|
+
initial_backoff: config.transport_initial_backoff,
|
|
78
|
+
max_backoff: config.transport_max_backoff,
|
|
79
|
+
backoff_multiplier: config.transport_backoff_multiplier,
|
|
80
|
+
jitter: config.transport_jitter,
|
|
81
|
+
ssl_verify: config.transport_ssl_verify
|
|
82
|
+
}.merge(custom_config)
|
|
83
|
+
|
|
84
|
+
HttpClient.new(uri, transport_config)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
require_relative "../http_client"
|
|
6
|
+
require_relative "../backtrace_processor"
|
|
7
|
+
|
|
8
|
+
module Lapsoss
|
|
9
|
+
module Adapters
|
|
10
|
+
# Adapter for Insight Hub (formerly Bugsnag)
|
|
11
|
+
# Note: The API endpoints still use bugsnag.com domains for backwards compatibility
|
|
12
|
+
class InsightHubAdapter < Base
|
|
13
|
+
NOTIFY_URI = "https://notify.bugsnag.com"
|
|
14
|
+
SESSION_URI = "https://sessions.bugsnag.com"
|
|
15
|
+
JSON_CONTENT_TYPE = "application/json"
|
|
16
|
+
|
|
17
|
+
def initialize(name, settings = {})
|
|
18
|
+
super(name, settings)
|
|
19
|
+
@api_key = settings[:api_key] || ENV["INSIGHT_HUB_API_KEY"] || ENV["BUGSNAG_API_KEY"]
|
|
20
|
+
@release_stage = settings[:release_stage] || Lapsoss.configuration.environment
|
|
21
|
+
@app_version = settings[:app_version]
|
|
22
|
+
@app_type = settings[:app_type] || "ruby"
|
|
23
|
+
|
|
24
|
+
validate_settings!
|
|
25
|
+
|
|
26
|
+
@notify_client = create_http_client(NOTIFY_URI)
|
|
27
|
+
@session_client = create_http_client(SESSION_URI) if settings[:enable_sessions]
|
|
28
|
+
@backtrace_processor = BacktraceProcessor.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def capture(event)
|
|
32
|
+
return unless enabled?
|
|
33
|
+
|
|
34
|
+
payload = build_payload(event)
|
|
35
|
+
return unless payload
|
|
36
|
+
|
|
37
|
+
headers = {
|
|
38
|
+
"Content-Type" => JSON_CONTENT_TYPE,
|
|
39
|
+
"Bugsnag-Api-Key" => @api_key,
|
|
40
|
+
"Bugsnag-Payload-Version" => "5.0",
|
|
41
|
+
"Bugsnag-Sent-At" => Time.now.utc.iso8601
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
@notify_client.post("/", body: JSON.generate(payload), headers: headers)
|
|
46
|
+
rescue DeliveryError => e
|
|
47
|
+
# Log the error and potentially notify error handler
|
|
48
|
+
Lapsoss.configuration.logger&.error("[Lapsoss::InsightHubAdapter] Failed to deliver event: #{e.message}")
|
|
49
|
+
Lapsoss.configuration.error_handler&.call(e)
|
|
50
|
+
|
|
51
|
+
# Re-raise to let the caller know delivery failed
|
|
52
|
+
raise
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def shutdown
|
|
57
|
+
@notify_client&.shutdown
|
|
58
|
+
@session_client&.shutdown
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def capabilities
|
|
63
|
+
super.merge(
|
|
64
|
+
code_context: true,
|
|
65
|
+
breadcrumbs: true,
|
|
66
|
+
sessions: true
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def build_payload(event)
|
|
73
|
+
{
|
|
74
|
+
apiKey: @api_key,
|
|
75
|
+
payloadVersion: "5.0",
|
|
76
|
+
notifier: {
|
|
77
|
+
name: "Lapsoss",
|
|
78
|
+
version: Lapsoss::VERSION,
|
|
79
|
+
url: "https://github.com/seuros/lapsoss"
|
|
80
|
+
},
|
|
81
|
+
events: [build_event(event)]
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_event(event)
|
|
86
|
+
base_event = {
|
|
87
|
+
app: {
|
|
88
|
+
version: @app_version,
|
|
89
|
+
releaseStage: @release_stage,
|
|
90
|
+
type: @app_type
|
|
91
|
+
},
|
|
92
|
+
device: {
|
|
93
|
+
hostname: Socket.gethostname,
|
|
94
|
+
runtimeVersions: {
|
|
95
|
+
ruby: RUBY_VERSION
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
severity: map_severity(event.level),
|
|
99
|
+
unhandled: event.context[:unhandled] || false,
|
|
100
|
+
severityReason: {
|
|
101
|
+
type: event.context[:unhandled] ? "unhandledException" : "handledException"
|
|
102
|
+
},
|
|
103
|
+
user: build_user(event),
|
|
104
|
+
context: event.context[:context],
|
|
105
|
+
groupingHash: event.context[:fingerprint],
|
|
106
|
+
breadcrumbs: build_breadcrumbs(event),
|
|
107
|
+
metaData: event.context[:extra] || {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case event.type
|
|
111
|
+
when :exception
|
|
112
|
+
base_event.merge(build_exception_data(event))
|
|
113
|
+
when :message
|
|
114
|
+
base_event.merge(build_message_data(event))
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build_exception_data(event)
|
|
119
|
+
{
|
|
120
|
+
exceptions: [{
|
|
121
|
+
errorClass: event.exception.class.name,
|
|
122
|
+
message: event.exception.message,
|
|
123
|
+
stacktrace: build_stacktrace(event.exception),
|
|
124
|
+
type: "ruby"
|
|
125
|
+
}]
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_message_data(event)
|
|
130
|
+
{
|
|
131
|
+
exceptions: [{
|
|
132
|
+
# Insight Hub (Bugsnag) requires exception format for messages - this isn't a real exception
|
|
133
|
+
# but rather a way to send structured log messages through their error API
|
|
134
|
+
errorClass: "LogMessage", # Clear indication this is a log message
|
|
135
|
+
message: event.message,
|
|
136
|
+
stacktrace: [], # No fake backtrace for log messages
|
|
137
|
+
type: "log" # Mark as log type, not ruby exception
|
|
138
|
+
}]
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def build_stacktrace(exception)
|
|
143
|
+
return [] unless exception
|
|
144
|
+
|
|
145
|
+
frames = @backtrace_processor.process_exception(exception)
|
|
146
|
+
@backtrace_processor.format_frames(frames, :bugsnag)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_breadcrumbs(event)
|
|
151
|
+
breadcrumbs = event.context[:breadcrumbs] || []
|
|
152
|
+
breadcrumbs.map do |crumb|
|
|
153
|
+
{
|
|
154
|
+
timestamp: crumb[:timestamp]&.iso8601 || Time.now.utc.iso8601,
|
|
155
|
+
name: crumb[:message] || crumb[:name],
|
|
156
|
+
type: crumb[:type] || "manual",
|
|
157
|
+
metaData: crumb[:data] || {}
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def build_user(event)
|
|
163
|
+
user = event.context[:user]
|
|
164
|
+
return unless user
|
|
165
|
+
|
|
166
|
+
{
|
|
167
|
+
id: user[:id]&.to_s,
|
|
168
|
+
name: user[:username] || user[:name],
|
|
169
|
+
email: user[:email]
|
|
170
|
+
}.compact
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def map_severity(level)
|
|
174
|
+
case level
|
|
175
|
+
when :debug, :info then "info"
|
|
176
|
+
when :warning, :warn then "warning"
|
|
177
|
+
when :error, :fatal, :critical then "error"
|
|
178
|
+
else "error"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def validate_settings!
|
|
183
|
+
validate_presence!(@api_key, "Insight Hub API key")
|
|
184
|
+
validate_api_key!(@api_key, "Insight Hub API key", format: :alphanumeric) if @api_key
|
|
185
|
+
validate_environment!(@app_version, "Insight Hub app version") if @app_version
|
|
186
|
+
validate_type!(@release_stage, [String, Symbol], "Insight Hub release stage") if @release_stage
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module Lapsoss
|
|
6
|
+
module Adapters
|
|
7
|
+
class LoggerAdapter < Base
|
|
8
|
+
def initialize(name, settings = {})
|
|
9
|
+
@logger = settings[:logger] || Logger.new($stdout)
|
|
10
|
+
super(name, settings)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def capabilities
|
|
14
|
+
super.merge(
|
|
15
|
+
breadcrumbs: true
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def capture(event)
|
|
20
|
+
case event.type
|
|
21
|
+
when :exception
|
|
22
|
+
@logger.error(format_exception(event.exception, event.context))
|
|
23
|
+
when :message
|
|
24
|
+
logger_level = map_level(event.level)
|
|
25
|
+
@logger.send(logger_level, format_message(event.message, event.context))
|
|
26
|
+
else
|
|
27
|
+
@logger.info("[LAPSOSS] Unhandled event type: #{event.type.inspect} | Event: #{event.to_h.inspect}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Log breadcrumbs if present in the event context
|
|
31
|
+
if event.context[:breadcrumbs]&.any?
|
|
32
|
+
event.context[:breadcrumbs].each do |breadcrumb|
|
|
33
|
+
breadcrumb_msg = "[BREADCRUMB] [#{breadcrumb[:type].upcase}] #{breadcrumb[:message]}"
|
|
34
|
+
breadcrumb_msg += " | #{breadcrumb[:metadata].inspect}" unless breadcrumb[:metadata].empty?
|
|
35
|
+
@logger.debug(breadcrumb_msg)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def format_exception(exception, context)
|
|
43
|
+
message = "[LAPSOSS] Exception: #{exception.class}: #{exception.message}"
|
|
44
|
+
message += "\n#{exception.backtrace.first(10).join("\n")}" if exception.backtrace
|
|
45
|
+
message += "\nContext: #{context.inspect}" unless context.empty?
|
|
46
|
+
message
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def format_message(message, context)
|
|
50
|
+
msg = "[LAPSOSS] #{message}"
|
|
51
|
+
msg += " | Context: #{context.inspect}" unless context.empty?
|
|
52
|
+
msg
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def map_level(level)
|
|
56
|
+
case level
|
|
57
|
+
when :debug then :debug
|
|
58
|
+
when :info then :info
|
|
59
|
+
when :warn, :warning then :warn
|
|
60
|
+
when :error then :error
|
|
61
|
+
when :fatal then :fatal
|
|
62
|
+
else :info
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require_relative "../http_client"
|
|
7
|
+
require_relative "../backtrace_processor"
|
|
8
|
+
|
|
9
|
+
module Lapsoss
|
|
10
|
+
module Adapters
|
|
11
|
+
class RollbarAdapter < Base
|
|
12
|
+
API_URI = "https://api.rollbar.com"
|
|
13
|
+
API_VERSION = "1"
|
|
14
|
+
JSON_CONTENT_TYPE = "application/json"
|
|
15
|
+
|
|
16
|
+
def initialize(name, settings = {})
|
|
17
|
+
super(name, settings)
|
|
18
|
+
@access_token = settings[:access_token] || ENV["ROLLBAR_ACCESS_TOKEN"]
|
|
19
|
+
@environment = settings[:environment] || Lapsoss.configuration.environment || "development"
|
|
20
|
+
|
|
21
|
+
validate_settings!
|
|
22
|
+
|
|
23
|
+
@client = create_http_client(API_URI)
|
|
24
|
+
@backtrace_processor = BacktraceProcessor.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def capture(event)
|
|
28
|
+
return unless enabled?
|
|
29
|
+
|
|
30
|
+
payload = build_payload(event)
|
|
31
|
+
return unless payload
|
|
32
|
+
|
|
33
|
+
path = "/api/#{API_VERSION}/item/"
|
|
34
|
+
headers = {
|
|
35
|
+
"Content-Type" => JSON_CONTENT_TYPE,
|
|
36
|
+
"X-Rollbar-Access-Token" => @access_token
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
@client.post(path, body: JSON.generate(payload), headers: headers)
|
|
41
|
+
rescue DeliveryError => e
|
|
42
|
+
# Log the error and potentially notify error handler
|
|
43
|
+
Lapsoss.configuration.logger&.error("[Lapsoss::RollbarAdapter] Failed to deliver event: #{e.message}")
|
|
44
|
+
Lapsoss.configuration.error_handler&.call(e)
|
|
45
|
+
|
|
46
|
+
# Re-raise to let the caller know delivery failed
|
|
47
|
+
raise
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def shutdown
|
|
52
|
+
@client&.shutdown
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def capabilities
|
|
57
|
+
super.merge(
|
|
58
|
+
code_context: true
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def build_payload(event)
|
|
65
|
+
data = {
|
|
66
|
+
environment: @environment,
|
|
67
|
+
body: build_body(event),
|
|
68
|
+
level: map_level(event.level),
|
|
69
|
+
timestamp: event.timestamp.to_i,
|
|
70
|
+
platform: "ruby",
|
|
71
|
+
language: "ruby",
|
|
72
|
+
framework: detect_framework,
|
|
73
|
+
context: event.context[:context],
|
|
74
|
+
request: event.context[:request],
|
|
75
|
+
person: build_person(event),
|
|
76
|
+
custom: event.context[:extra] || {},
|
|
77
|
+
title: event.message || (event.exception&.message if event.type == :exception),
|
|
78
|
+
uuid: SecureRandom.uuid,
|
|
79
|
+
notifier: {
|
|
80
|
+
name: "lapsoss",
|
|
81
|
+
version: Lapsoss::VERSION
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Only add fingerprint if it exists
|
|
86
|
+
data[:fingerprint] = event.context[:fingerprint].to_s if event.context[:fingerprint]
|
|
87
|
+
|
|
88
|
+
{ data: data }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_body(event)
|
|
92
|
+
case event.type
|
|
93
|
+
when :exception
|
|
94
|
+
{
|
|
95
|
+
trace: {
|
|
96
|
+
frames: build_backtrace_frames(event.exception),
|
|
97
|
+
exception: {
|
|
98
|
+
class: event.exception.class.name,
|
|
99
|
+
message: event.exception.message,
|
|
100
|
+
description: event.exception.message
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
when :message
|
|
105
|
+
{
|
|
106
|
+
message: {
|
|
107
|
+
body: event.message
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def build_backtrace_frames(exception)
|
|
114
|
+
return [] unless exception
|
|
115
|
+
|
|
116
|
+
frames = @backtrace_processor.process_exception(exception, follow_cause: true)
|
|
117
|
+
formatted_frames = @backtrace_processor.format_frames(frames, :rollbar)
|
|
118
|
+
# Rollbar expects frames in reverse order (most recent first)
|
|
119
|
+
formatted_frames.reverse
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_person(event)
|
|
123
|
+
user = event.context[:user]
|
|
124
|
+
return unless user
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
id: user[:id]&.to_s,
|
|
128
|
+
username: user[:username],
|
|
129
|
+
email: user[:email]
|
|
130
|
+
}.compact
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def map_level(level)
|
|
134
|
+
case level
|
|
135
|
+
when :debug then "debug"
|
|
136
|
+
when :info then "info"
|
|
137
|
+
when :warning, :warn then "warning"
|
|
138
|
+
when :error then "error"
|
|
139
|
+
when :fatal, :critical then "critical"
|
|
140
|
+
else "error"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def detect_framework
|
|
145
|
+
return "rails" if defined?(Rails)
|
|
146
|
+
return "sinatra" if defined?(Sinatra)
|
|
147
|
+
"ruby"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_settings!
|
|
151
|
+
validate_presence!(@access_token, "Rollbar access token")
|
|
152
|
+
validate_api_key!(@access_token, "Rollbar access token", format: :alphanumeric) if @access_token
|
|
153
|
+
validate_environment!(@environment, "Rollbar environment") if @environment
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|