sentry-ruby 5.10.0 → 5.26.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/.rspec +3 -1
- data/Gemfile +12 -13
- data/README.md +26 -11
- data/Rakefile +9 -11
- data/bin/console +2 -0
- data/lib/sentry/attachment.rb +40 -0
- data/lib/sentry/background_worker.rb +11 -5
- data/lib/sentry/backpressure_monitor.rb +45 -0
- data/lib/sentry/backtrace.rb +12 -9
- data/lib/sentry/baggage.rb +7 -7
- data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
- data/lib/sentry/breadcrumb.rb +13 -6
- data/lib/sentry/check_in_event.rb +61 -0
- data/lib/sentry/client.rb +214 -25
- data/lib/sentry/configuration.rb +221 -38
- data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
- data/lib/sentry/cron/configuration.rb +23 -0
- data/lib/sentry/cron/monitor_check_ins.rb +77 -0
- data/lib/sentry/cron/monitor_config.rb +53 -0
- data/lib/sentry/cron/monitor_schedule.rb +42 -0
- data/lib/sentry/dsn.rb +4 -4
- data/lib/sentry/envelope/item.rb +88 -0
- data/lib/sentry/envelope.rb +2 -68
- data/lib/sentry/error_event.rb +2 -2
- data/lib/sentry/event.rb +28 -47
- data/lib/sentry/excon/middleware.rb +77 -0
- data/lib/sentry/excon.rb +10 -0
- data/lib/sentry/faraday.rb +77 -0
- data/lib/sentry/graphql.rb +9 -0
- data/lib/sentry/hub.rb +138 -6
- data/lib/sentry/integrable.rb +10 -0
- data/lib/sentry/interface.rb +1 -0
- data/lib/sentry/interfaces/exception.rb +5 -3
- data/lib/sentry/interfaces/mechanism.rb +20 -0
- data/lib/sentry/interfaces/request.rb +8 -8
- data/lib/sentry/interfaces/single_exception.rb +13 -9
- data/lib/sentry/interfaces/stacktrace.rb +3 -1
- data/lib/sentry/interfaces/stacktrace_builder.rb +23 -2
- data/lib/sentry/linecache.rb +3 -3
- data/lib/sentry/log_event.rb +206 -0
- data/lib/sentry/log_event_buffer.rb +75 -0
- data/lib/sentry/logger.rb +1 -1
- data/lib/sentry/metrics/aggregator.rb +248 -0
- data/lib/sentry/metrics/configuration.rb +47 -0
- data/lib/sentry/metrics/counter_metric.rb +25 -0
- data/lib/sentry/metrics/distribution_metric.rb +25 -0
- data/lib/sentry/metrics/gauge_metric.rb +35 -0
- data/lib/sentry/metrics/local_aggregator.rb +53 -0
- data/lib/sentry/metrics/metric.rb +19 -0
- data/lib/sentry/metrics/set_metric.rb +28 -0
- data/lib/sentry/metrics/timing.rb +51 -0
- data/lib/sentry/metrics.rb +56 -0
- data/lib/sentry/net/http.rb +27 -44
- data/lib/sentry/profiler/helpers.rb +46 -0
- data/lib/sentry/profiler.rb +41 -60
- data/lib/sentry/propagation_context.rb +135 -0
- data/lib/sentry/puma.rb +12 -5
- data/lib/sentry/rack/capture_exceptions.rb +17 -8
- data/lib/sentry/rack.rb +2 -2
- data/lib/sentry/rake.rb +4 -15
- data/lib/sentry/redis.rb +10 -4
- data/lib/sentry/release_detector.rb +5 -5
- data/lib/sentry/rspec.rb +91 -0
- data/lib/sentry/scope.rb +75 -39
- data/lib/sentry/session.rb +2 -2
- data/lib/sentry/session_flusher.rb +15 -43
- data/lib/sentry/span.rb +92 -8
- data/lib/sentry/std_lib_logger.rb +50 -0
- data/lib/sentry/structured_logger.rb +138 -0
- data/lib/sentry/test_helper.rb +42 -13
- data/lib/sentry/threaded_periodic_worker.rb +39 -0
- data/lib/sentry/transaction.rb +44 -43
- data/lib/sentry/transaction_event.rb +10 -6
- data/lib/sentry/transport/configuration.rb +73 -1
- data/lib/sentry/transport/http_transport.rb +71 -41
- data/lib/sentry/transport/spotlight_transport.rb +50 -0
- data/lib/sentry/transport.rb +53 -49
- data/lib/sentry/utils/argument_checking_helper.rb +12 -0
- data/lib/sentry/utils/env_helper.rb +21 -0
- data/lib/sentry/utils/http_tracing.rb +74 -0
- data/lib/sentry/utils/logging_helper.rb +10 -7
- data/lib/sentry/utils/real_ip.rb +2 -2
- data/lib/sentry/utils/request_id.rb +1 -1
- data/lib/sentry/utils/uuid.rb +13 -0
- data/lib/sentry/vernier/output.rb +89 -0
- data/lib/sentry/vernier/profiler.rb +132 -0
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +206 -35
- data/sentry-ruby-core.gemspec +3 -1
- data/sentry-ruby.gemspec +15 -6
- metadata +61 -11
data/lib/sentry/transport.rb
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "json"
|
4
|
-
require "base64"
|
5
4
|
require "sentry/envelope"
|
6
5
|
|
7
6
|
module Sentry
|
8
7
|
class Transport
|
9
|
-
PROTOCOL_VERSION =
|
8
|
+
PROTOCOL_VERSION = "7"
|
10
9
|
USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
|
11
10
|
CLIENT_REPORT_INTERVAL = 30
|
12
11
|
|
@@ -18,18 +17,17 @@ module Sentry
|
|
18
17
|
:network_error,
|
19
18
|
:sample_rate,
|
20
19
|
:before_send,
|
21
|
-
:event_processor
|
20
|
+
:event_processor,
|
21
|
+
:insufficient_data,
|
22
|
+
:backpressure
|
22
23
|
]
|
23
24
|
|
24
25
|
include LoggingHelper
|
25
26
|
|
26
27
|
attr_reader :rate_limits, :discarded_events, :last_client_report_sent
|
27
28
|
|
28
|
-
# @deprecated Use Sentry.logger to retrieve the current logger instead.
|
29
|
-
attr_reader :logger
|
30
|
-
|
31
29
|
def initialize(configuration)
|
32
|
-
@
|
30
|
+
@sdk_logger = configuration.sdk_logger
|
33
31
|
@transport_configuration = configuration.transport
|
34
32
|
@dsn = configuration.dsn
|
35
33
|
@rate_limits = {}
|
@@ -60,7 +58,7 @@ module Sentry
|
|
60
58
|
data, serialized_items = serialize_envelope(envelope)
|
61
59
|
|
62
60
|
if data
|
63
|
-
|
61
|
+
log_debug("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
|
64
62
|
send_data(data)
|
65
63
|
end
|
66
64
|
end
|
@@ -73,7 +71,7 @@ module Sentry
|
|
73
71
|
result, oversized = item.serialize
|
74
72
|
|
75
73
|
if oversized
|
76
|
-
|
74
|
+
log_debug("Envelope item [#{item.type}] is still oversized after size reduction: {#{item.size_breakdown}}")
|
77
75
|
|
78
76
|
next
|
79
77
|
end
|
@@ -87,18 +85,9 @@ module Sentry
|
|
87
85
|
[data, serialized_items]
|
88
86
|
end
|
89
87
|
|
90
|
-
def is_rate_limited?(
|
88
|
+
def is_rate_limited?(data_category)
|
91
89
|
# check category-specific limit
|
92
|
-
category_delay =
|
93
|
-
case item_type
|
94
|
-
when "transaction"
|
95
|
-
@rate_limits["transaction"]
|
96
|
-
when "sessions"
|
97
|
-
@rate_limits["session"]
|
98
|
-
else
|
99
|
-
@rate_limits["error"]
|
100
|
-
end
|
101
|
-
|
90
|
+
category_delay = @rate_limits[data_category]
|
102
91
|
# check universal limit if not category limit
|
103
92
|
universal_delay = @rate_limits[nil]
|
104
93
|
|
@@ -118,16 +107,8 @@ module Sentry
|
|
118
107
|
!!delay && delay > Time.now
|
119
108
|
end
|
120
109
|
|
121
|
-
def
|
122
|
-
|
123
|
-
fields = {
|
124
|
-
'sentry_version' => PROTOCOL_VERSION,
|
125
|
-
'sentry_client' => USER_AGENT,
|
126
|
-
'sentry_timestamp' => now,
|
127
|
-
'sentry_key' => @dsn.public_key
|
128
|
-
}
|
129
|
-
fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
|
130
|
-
'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
|
110
|
+
def any_rate_limited?
|
111
|
+
@rate_limits.values.any? { |t| t && t > Time.now }
|
131
112
|
end
|
132
113
|
|
133
114
|
def envelope_from_event(event)
|
@@ -143,54 +124,76 @@ module Sentry
|
|
143
124
|
sent_at: Sentry.utc_now.iso8601
|
144
125
|
}
|
145
126
|
|
146
|
-
if event.is_a?(
|
127
|
+
if event.is_a?(Event) && event.dynamic_sampling_context
|
147
128
|
envelope_headers[:trace] = event.dynamic_sampling_context
|
148
129
|
end
|
149
130
|
|
150
131
|
envelope = Envelope.new(envelope_headers)
|
151
132
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
133
|
+
if event.is_a?(LogEvent)
|
134
|
+
envelope.add_item(
|
135
|
+
{
|
136
|
+
type: "log",
|
137
|
+
item_count: 1,
|
138
|
+
content_type: "application/vnd.sentry.items.log+json"
|
139
|
+
},
|
140
|
+
{ items: [event_payload] }
|
141
|
+
)
|
142
|
+
else
|
143
|
+
envelope.add_item(
|
144
|
+
{ type: item_type, content_type: "application/json" },
|
145
|
+
event_payload
|
146
|
+
)
|
147
|
+
end
|
156
148
|
|
157
149
|
if event.is_a?(TransactionEvent) && event.profile
|
158
150
|
envelope.add_item(
|
159
|
-
{ type:
|
151
|
+
{ type: "profile", content_type: "application/json" },
|
160
152
|
event.profile
|
161
153
|
)
|
162
154
|
end
|
163
155
|
|
156
|
+
if event.is_a?(Event) && event.attachments.any?
|
157
|
+
event.attachments.each do |attachment|
|
158
|
+
envelope.add_item(attachment.to_envelope_headers, attachment.payload)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
164
162
|
client_report_headers, client_report_payload = fetch_pending_client_report
|
165
163
|
envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
|
166
164
|
|
167
165
|
envelope
|
168
166
|
end
|
169
167
|
|
170
|
-
def record_lost_event(reason,
|
168
|
+
def record_lost_event(reason, data_category, num: 1)
|
171
169
|
return unless @send_client_reports
|
172
170
|
return unless CLIENT_REPORT_REASONS.include?(reason)
|
173
171
|
|
174
|
-
@discarded_events[[reason,
|
172
|
+
@discarded_events[[reason, data_category]] += num
|
173
|
+
end
|
174
|
+
|
175
|
+
def flush
|
176
|
+
client_report_headers, client_report_payload = fetch_pending_client_report(force: true)
|
177
|
+
return unless client_report_headers
|
178
|
+
|
179
|
+
envelope = Envelope.new
|
180
|
+
envelope.add_item(client_report_headers, client_report_payload)
|
181
|
+
send_envelope(envelope)
|
175
182
|
end
|
176
183
|
|
177
184
|
private
|
178
185
|
|
179
|
-
def fetch_pending_client_report
|
186
|
+
def fetch_pending_client_report(force: false)
|
180
187
|
return nil unless @send_client_reports
|
181
|
-
return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
|
188
|
+
return nil if !force && @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
|
182
189
|
return nil if @discarded_events.empty?
|
183
190
|
|
184
191
|
discarded_events_hash = @discarded_events.map do |key, val|
|
185
|
-
reason,
|
186
|
-
|
187
|
-
# 'event' has to be mapped to 'error'
|
188
|
-
category = type == 'transaction' ? 'transaction' : 'error'
|
189
|
-
|
192
|
+
reason, category = key
|
190
193
|
{ reason: reason, category: category, quantity: val }
|
191
194
|
end
|
192
195
|
|
193
|
-
item_header = { type:
|
196
|
+
item_header = { type: "client_report" }
|
194
197
|
item_payload = {
|
195
198
|
timestamp: Sentry.utc_now.iso8601,
|
196
199
|
discarded_events: discarded_events_hash
|
@@ -204,9 +207,9 @@ module Sentry
|
|
204
207
|
|
205
208
|
def reject_rate_limited_items(envelope)
|
206
209
|
envelope.items.reject! do |item|
|
207
|
-
if is_rate_limited?(item.
|
208
|
-
|
209
|
-
record_lost_event(:ratelimit_backoff, item.
|
210
|
+
if is_rate_limited?(item.data_category)
|
211
|
+
log_debug("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
|
212
|
+
record_lost_event(:ratelimit_backoff, item.data_category)
|
210
213
|
|
211
214
|
true
|
212
215
|
else
|
@@ -219,3 +222,4 @@ end
|
|
219
222
|
|
220
223
|
require "sentry/transport/dummy_transport"
|
221
224
|
require "sentry/transport/http_transport"
|
225
|
+
require "sentry/transport/spotlight_transport"
|
@@ -9,5 +9,17 @@ module Sentry
|
|
9
9
|
raise ArgumentError, "expect the argument to be a #{expected_types.join(' or ')}, got #{argument.class} (#{argument.inspect})"
|
10
10
|
end
|
11
11
|
end
|
12
|
+
|
13
|
+
def check_argument_includes!(argument, values)
|
14
|
+
unless values.include?(argument)
|
15
|
+
raise ArgumentError, "expect the argument to be one of #{values.map(&:inspect).join(' or ')}, got #{argument.inspect}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def check_callable!(name, value)
|
20
|
+
unless value == nil || value.respond_to?(:call)
|
21
|
+
raise ArgumentError, "#{name} must be callable (or nil to disable)"
|
22
|
+
end
|
23
|
+
end
|
12
24
|
end
|
13
25
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Utils
|
5
|
+
module EnvHelper
|
6
|
+
TRUTHY_ENV_VALUES = %w[t true yes y 1 on].freeze
|
7
|
+
FALSY_ENV_VALUES = %w[f false no n 0 off].freeze
|
8
|
+
|
9
|
+
def self.env_to_bool(value, strict: false)
|
10
|
+
value = value.to_s
|
11
|
+
normalized = value.downcase
|
12
|
+
|
13
|
+
return false if FALSY_ENV_VALUES.include?(normalized)
|
14
|
+
|
15
|
+
return true if TRUTHY_ENV_VALUES.include?(normalized)
|
16
|
+
|
17
|
+
strict ? nil : !(value.nil? || value.empty?)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Utils
|
5
|
+
module HttpTracing
|
6
|
+
def set_span_info(sentry_span, request_info, response_status)
|
7
|
+
sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
|
8
|
+
sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
|
9
|
+
sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method])
|
10
|
+
sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query]
|
11
|
+
sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, response_status)
|
12
|
+
end
|
13
|
+
|
14
|
+
def set_propagation_headers(req)
|
15
|
+
Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v }
|
16
|
+
end
|
17
|
+
|
18
|
+
def record_sentry_breadcrumb(request_info, response_status)
|
19
|
+
crumb = Sentry::Breadcrumb.new(
|
20
|
+
level: get_level(response_status),
|
21
|
+
category: self.class::BREADCRUMB_CATEGORY,
|
22
|
+
type: "info",
|
23
|
+
data: { status: response_status, **request_info }
|
24
|
+
)
|
25
|
+
|
26
|
+
Sentry.add_breadcrumb(crumb)
|
27
|
+
end
|
28
|
+
|
29
|
+
def record_sentry_breadcrumb?
|
30
|
+
Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
|
31
|
+
end
|
32
|
+
|
33
|
+
def propagate_trace?(url)
|
34
|
+
url &&
|
35
|
+
Sentry.initialized? &&
|
36
|
+
Sentry.configuration.propagate_traces &&
|
37
|
+
Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Kindly borrowed from Rack::Utils
|
41
|
+
def build_nested_query(value, prefix = nil)
|
42
|
+
case value
|
43
|
+
when Array
|
44
|
+
value.map { |v|
|
45
|
+
build_nested_query(v, "#{prefix}[]")
|
46
|
+
}.join("&")
|
47
|
+
when Hash
|
48
|
+
value.map { |k, v|
|
49
|
+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
50
|
+
}.delete_if(&:empty?).join("&")
|
51
|
+
when nil
|
52
|
+
URI.encode_www_form_component(prefix)
|
53
|
+
else
|
54
|
+
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
55
|
+
"#{URI.encode_www_form_component(prefix)}=#{URI.encode_www_form_component(value)}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def get_level(status)
|
62
|
+
return :info unless status && status.is_a?(Integer)
|
63
|
+
|
64
|
+
if status >= 500
|
65
|
+
:error
|
66
|
+
elsif status >= 400
|
67
|
+
:warning
|
68
|
+
else
|
69
|
+
:info
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -1,26 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Sentry
|
4
|
+
# @private
|
4
5
|
module LoggingHelper
|
6
|
+
# @!visibility private
|
7
|
+
attr_reader :sdk_logger
|
8
|
+
|
9
|
+
# @!visibility private
|
5
10
|
def log_error(message, exception, debug: false)
|
6
11
|
message = "#{message}: #{exception.message}"
|
7
12
|
message += "\n#{exception.backtrace.join("\n")}" if debug
|
8
13
|
|
9
|
-
|
14
|
+
sdk_logger.error(LOGGER_PROGNAME) do
|
10
15
|
message
|
11
16
|
end
|
12
17
|
end
|
13
18
|
|
14
|
-
|
15
|
-
@logger.info(LOGGER_PROGNAME) { message }
|
16
|
-
end
|
17
|
-
|
19
|
+
# @!visibility private
|
18
20
|
def log_debug(message)
|
19
|
-
|
21
|
+
sdk_logger.debug(LOGGER_PROGNAME) { message }
|
20
22
|
end
|
21
23
|
|
24
|
+
# @!visibility private
|
22
25
|
def log_warn(message)
|
23
|
-
|
26
|
+
sdk_logger.warn(LOGGER_PROGNAME) { message }
|
24
27
|
end
|
25
28
|
end
|
26
29
|
end
|
data/lib/sentry/utils/real_ip.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "ipaddr"
|
4
4
|
|
5
5
|
# Based on ActionDispatch::RemoteIp. All security-related precautions from that
|
6
6
|
# middleware have been removed, because the Event IP just needs to be accurate,
|
@@ -15,7 +15,7 @@ module Sentry
|
|
15
15
|
"fc00::/7", # private IPv6 range fc00::/7
|
16
16
|
"10.0.0.0/8", # private IPv4 range 10.x.x.x
|
17
17
|
"172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
|
18
|
-
"192.168.0.0/16"
|
18
|
+
"192.168.0.0/16" # private IPv4 range 192.168.x.x
|
19
19
|
]
|
20
20
|
|
21
21
|
attr_reader :ip
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Sentry
|
4
4
|
module Utils
|
5
5
|
module RequestId
|
6
|
-
REQUEST_ID_HEADERS = %w
|
6
|
+
REQUEST_ID_HEADERS = %w[action_dispatch.request_id HTTP_X_REQUEST_ID].freeze
|
7
7
|
|
8
8
|
# Request ID based on ActionDispatch::RequestId
|
9
9
|
def self.read_from(env)
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "rbconfig"
|
5
|
+
|
6
|
+
module Sentry
|
7
|
+
module Vernier
|
8
|
+
class Output
|
9
|
+
include Profiler::Helpers
|
10
|
+
|
11
|
+
attr_reader :profile
|
12
|
+
|
13
|
+
def initialize(profile, project_root:, in_app_pattern:, app_dirs_pattern:)
|
14
|
+
@profile = profile
|
15
|
+
@project_root = project_root
|
16
|
+
@in_app_pattern = in_app_pattern
|
17
|
+
@app_dirs_pattern = app_dirs_pattern
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
@to_h ||= {
|
22
|
+
frames: frames,
|
23
|
+
stacks: stacks,
|
24
|
+
samples: samples,
|
25
|
+
thread_metadata: thread_metadata
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def thread_metadata
|
32
|
+
profile.threads.map { |thread_id, thread_info|
|
33
|
+
[thread_id, { name: thread_info[:name] }]
|
34
|
+
}.to_h
|
35
|
+
end
|
36
|
+
|
37
|
+
def samples
|
38
|
+
profile.threads.flat_map { |thread_id, thread_info|
|
39
|
+
started_at = thread_info[:started_at]
|
40
|
+
samples, timestamps = thread_info.values_at(:samples, :timestamps)
|
41
|
+
|
42
|
+
samples.zip(timestamps).map { |stack_id, timestamp|
|
43
|
+
elapsed_since_start_ns = timestamp - started_at
|
44
|
+
|
45
|
+
next if elapsed_since_start_ns < 0
|
46
|
+
|
47
|
+
{
|
48
|
+
thread_id: thread_id.to_s,
|
49
|
+
stack_id: stack_id,
|
50
|
+
elapsed_since_start_ns: elapsed_since_start_ns.to_s
|
51
|
+
}
|
52
|
+
}.compact
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def frames
|
57
|
+
funcs = stack_table_hash[:frame_table].fetch(:func)
|
58
|
+
lines = stack_table_hash[:func_table].fetch(:first_line)
|
59
|
+
|
60
|
+
funcs.map do |idx|
|
61
|
+
function, mod = split_module(stack_table_hash[:func_table][:name][idx])
|
62
|
+
|
63
|
+
abs_path = stack_table_hash[:func_table][:filename][idx]
|
64
|
+
in_app = in_app?(abs_path)
|
65
|
+
filename = compute_filename(abs_path, in_app)
|
66
|
+
|
67
|
+
{
|
68
|
+
function: function,
|
69
|
+
module: mod,
|
70
|
+
filename: filename,
|
71
|
+
abs_path: abs_path,
|
72
|
+
lineno: (lineno = lines[idx]) > 0 ? lineno : nil,
|
73
|
+
in_app: in_app
|
74
|
+
}.compact
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def stacks
|
79
|
+
profile._stack_table.stack_count.times.map do |stack_id|
|
80
|
+
profile.stack(stack_id).frames.map(&:idx)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def stack_table_hash
|
85
|
+
@stack_table_hash ||= profile._stack_table.to_h
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require_relative "../profiler/helpers"
|
5
|
+
require_relative "output"
|
6
|
+
require "sentry/utils/uuid"
|
7
|
+
|
8
|
+
module Sentry
|
9
|
+
module Vernier
|
10
|
+
class Profiler
|
11
|
+
EMPTY_RESULT = {}.freeze
|
12
|
+
|
13
|
+
attr_reader :started, :event_id, :result
|
14
|
+
|
15
|
+
def initialize(configuration)
|
16
|
+
@event_id = Utils.uuid
|
17
|
+
|
18
|
+
@started = false
|
19
|
+
@sampled = nil
|
20
|
+
|
21
|
+
@profiling_enabled = defined?(Vernier) && configuration.profiling_enabled?
|
22
|
+
@profiles_sample_rate = configuration.profiles_sample_rate
|
23
|
+
@project_root = configuration.project_root
|
24
|
+
@app_dirs_pattern = configuration.app_dirs_pattern
|
25
|
+
@in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_initial_sample_decision(transaction_sampled)
|
29
|
+
unless @profiling_enabled
|
30
|
+
@sampled = false
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
unless transaction_sampled
|
35
|
+
@sampled = false
|
36
|
+
log("Discarding profile because transaction not sampled")
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
case @profiles_sample_rate
|
41
|
+
when 0.0
|
42
|
+
@sampled = false
|
43
|
+
log("Discarding profile because sample_rate is 0")
|
44
|
+
return
|
45
|
+
when 1.0
|
46
|
+
@sampled = true
|
47
|
+
return
|
48
|
+
else
|
49
|
+
@sampled = Random.rand < @profiles_sample_rate
|
50
|
+
end
|
51
|
+
|
52
|
+
log("Discarding profile due to sampling decision") unless @sampled
|
53
|
+
end
|
54
|
+
|
55
|
+
def start
|
56
|
+
return unless @sampled
|
57
|
+
return if @started
|
58
|
+
|
59
|
+
@started = ::Vernier.start_profile
|
60
|
+
|
61
|
+
log("Started")
|
62
|
+
|
63
|
+
@started
|
64
|
+
rescue RuntimeError => e
|
65
|
+
# TODO: once Vernier raises something more dedicated, we should catch that instead
|
66
|
+
if e.message.include?("Profile already started")
|
67
|
+
log("Not started since running elsewhere")
|
68
|
+
else
|
69
|
+
log("Failed to start: #{e.message}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def stop
|
74
|
+
return unless @sampled
|
75
|
+
return unless @started
|
76
|
+
|
77
|
+
@result = ::Vernier.stop_profile
|
78
|
+
@started = false
|
79
|
+
|
80
|
+
log("Stopped")
|
81
|
+
rescue RuntimeError => e
|
82
|
+
if e.message.include?("Profile not started")
|
83
|
+
log("Not stopped since not started")
|
84
|
+
else
|
85
|
+
log("Failed to stop Vernier: #{e.message}")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def active_thread_id
|
90
|
+
Thread.current.object_id
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_hash
|
94
|
+
unless @sampled
|
95
|
+
record_lost_event(:sample_rate)
|
96
|
+
return EMPTY_RESULT
|
97
|
+
end
|
98
|
+
|
99
|
+
return EMPTY_RESULT unless result
|
100
|
+
|
101
|
+
{ **profile_meta, profile: output.to_h }
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def log(message)
|
107
|
+
Sentry.sdk_logger.debug(LOGGER_PROGNAME) { "[Profiler::Vernier] #{message}" }
|
108
|
+
end
|
109
|
+
|
110
|
+
def record_lost_event(reason)
|
111
|
+
Sentry.get_current_client&.transport&.record_lost_event(reason, "profile")
|
112
|
+
end
|
113
|
+
|
114
|
+
def profile_meta
|
115
|
+
{
|
116
|
+
event_id: @event_id,
|
117
|
+
version: "1",
|
118
|
+
platform: "ruby"
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
def output
|
123
|
+
@output ||= Output.new(
|
124
|
+
result,
|
125
|
+
project_root: @project_root,
|
126
|
+
app_dirs_pattern: @app_dirs_pattern,
|
127
|
+
in_app_pattern: @in_app_pattern
|
128
|
+
)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/sentry/version.rb
CHANGED