sentry-ruby 5.16.1 → 5.21.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/Gemfile +6 -0
- data/README.md +20 -10
- data/Rakefile +3 -1
- data/bin/console +2 -0
- data/lib/sentry/attachment.rb +40 -0
- data/lib/sentry/background_worker.rb +1 -1
- data/lib/sentry/backpressure_monitor.rb +2 -32
- data/lib/sentry/backtrace.rb +10 -8
- data/lib/sentry/baggage.rb +7 -7
- data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
- data/lib/sentry/check_in_event.rb +5 -5
- data/lib/sentry/client.rb +61 -11
- data/lib/sentry/configuration.rb +77 -31
- data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
- data/lib/sentry/cron/monitor_check_ins.rb +3 -1
- data/lib/sentry/cron/monitor_config.rb +1 -1
- data/lib/sentry/cron/monitor_schedule.rb +1 -1
- 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 +20 -18
- data/lib/sentry/faraday.rb +77 -0
- data/lib/sentry/graphql.rb +9 -0
- data/lib/sentry/hub.rb +23 -3
- data/lib/sentry/integrable.rb +4 -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 +7 -7
- data/lib/sentry/interfaces/single_exception.rb +9 -7
- data/lib/sentry/interfaces/stacktrace.rb +3 -1
- data/lib/sentry/interfaces/stacktrace_builder.rb +23 -2
- 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 +43 -0
- data/lib/sentry/metrics.rb +56 -0
- data/lib/sentry/net/http.rb +18 -39
- data/lib/sentry/profiler/helpers.rb +46 -0
- data/lib/sentry/profiler.rb +25 -56
- data/lib/sentry/propagation_context.rb +10 -9
- data/lib/sentry/puma.rb +1 -1
- data/lib/sentry/rack/capture_exceptions.rb +16 -4
- data/lib/sentry/rack.rb +2 -2
- data/lib/sentry/rake.rb +4 -2
- data/lib/sentry/redis.rb +2 -1
- data/lib/sentry/release_detector.rb +4 -4
- data/lib/sentry/scope.rb +36 -26
- data/lib/sentry/session.rb +2 -2
- data/lib/sentry/session_flusher.rb +7 -39
- data/lib/sentry/span.rb +46 -5
- data/lib/sentry/test_helper.rb +5 -2
- data/lib/sentry/threaded_periodic_worker.rb +39 -0
- data/lib/sentry/transaction.rb +19 -17
- data/lib/sentry/transaction_event.rb +6 -2
- data/lib/sentry/transport/configuration.rb +0 -1
- data/lib/sentry/transport/http_transport.rb +12 -12
- data/lib/sentry/transport.rb +18 -26
- data/lib/sentry/utils/argument_checking_helper.rb +6 -0
- data/lib/sentry/utils/env_helper.rb +21 -0
- data/lib/sentry/utils/http_tracing.rb +41 -0
- data/lib/sentry/utils/logging_helper.rb +0 -4
- data/lib/sentry/utils/real_ip.rb +2 -2
- data/lib/sentry/utils/request_id.rb +1 -1
- data/lib/sentry/vernier/output.rb +89 -0
- data/lib/sentry/vernier/profiler.rb +125 -0
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +38 -6
- data/sentry-ruby-core.gemspec +3 -1
- data/sentry-ruby.gemspec +15 -6
- metadata +44 -7
data/lib/sentry/hub.rb
CHANGED
@@ -73,7 +73,13 @@ module Sentry
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def pop_scope
|
76
|
-
@stack.
|
76
|
+
if @stack.size > 1
|
77
|
+
@stack.pop
|
78
|
+
else
|
79
|
+
# We never want to enter a situation where we have no scope and no client
|
80
|
+
client = current_client
|
81
|
+
@stack = [Layer.new(client, Scope.new)]
|
82
|
+
end
|
77
83
|
end
|
78
84
|
|
79
85
|
def start_transaction(transaction: nil, custom_sampling_context: {}, instrumenter: :sentry, **options)
|
@@ -193,7 +199,14 @@ module Sentry
|
|
193
199
|
elsif custom_scope = options[:scope]
|
194
200
|
scope.update_from_scope(custom_scope)
|
195
201
|
elsif !options.empty?
|
196
|
-
scope.update_from_options(**options)
|
202
|
+
unsupported_option_keys = scope.update_from_options(**options)
|
203
|
+
|
204
|
+
unless unsupported_option_keys.empty?
|
205
|
+
configuration.log_debug <<~MSG
|
206
|
+
Options #{unsupported_option_keys} are not supported and will not be applied to the event.
|
207
|
+
You may want to set them under the `extra` option.
|
208
|
+
MSG
|
209
|
+
end
|
197
210
|
end
|
198
211
|
|
199
212
|
event = current_client.capture_event(event, scope, hint)
|
@@ -207,6 +220,7 @@ module Sentry
|
|
207
220
|
end
|
208
221
|
|
209
222
|
def add_breadcrumb(breadcrumb, hint: {})
|
223
|
+
return unless current_client
|
210
224
|
return unless configuration.enabled_in_current_env?
|
211
225
|
|
212
226
|
if before_breadcrumb = current_client.configuration.before_breadcrumb
|
@@ -245,7 +259,7 @@ module Sentry
|
|
245
259
|
end
|
246
260
|
|
247
261
|
def with_session_tracking(&block)
|
248
|
-
return yield unless configuration.
|
262
|
+
return yield unless configuration.session_tracking?
|
249
263
|
|
250
264
|
start_session
|
251
265
|
yield
|
@@ -279,6 +293,12 @@ module Sentry
|
|
279
293
|
headers
|
280
294
|
end
|
281
295
|
|
296
|
+
def get_trace_propagation_meta
|
297
|
+
get_trace_propagation_headers.map do |k, v|
|
298
|
+
"<meta name=\"#{k}\" content=\"#{v}\">"
|
299
|
+
end.join("\n")
|
300
|
+
end
|
301
|
+
|
282
302
|
def continue_trace(env, **options)
|
283
303
|
configure_scope { |s| s.generate_propagation_context(env) }
|
284
304
|
|
data/lib/sentry/integrable.rb
CHANGED
@@ -14,6 +14,10 @@ module Sentry
|
|
14
14
|
def capture_exception(exception, **options, &block)
|
15
15
|
options[:hint] ||= {}
|
16
16
|
options[:hint][:integration] = integration_name
|
17
|
+
|
18
|
+
# within an integration, we usually intercept uncaught exceptions so we set handled to false.
|
19
|
+
options[:hint][:mechanism] ||= Sentry::Mechanism.new(type: integration_name, handled: false)
|
20
|
+
|
17
21
|
Sentry.capture_exception(exception, **options, &block)
|
18
22
|
end
|
19
23
|
|
data/lib/sentry/interface.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "set"
|
3
4
|
|
4
5
|
module Sentry
|
@@ -23,17 +24,18 @@ module Sentry
|
|
23
24
|
# @param stacktrace_builder [StacktraceBuilder]
|
24
25
|
# @see SingleExceptionInterface#build_with_stacktrace
|
25
26
|
# @see SingleExceptionInterface#initialize
|
27
|
+
# @param mechanism [Mechanism]
|
26
28
|
# @return [ExceptionInterface]
|
27
|
-
def self.build(exception:, stacktrace_builder:)
|
29
|
+
def self.build(exception:, stacktrace_builder:, mechanism:)
|
28
30
|
exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse
|
29
31
|
processed_backtrace_ids = Set.new
|
30
32
|
|
31
33
|
exceptions = exceptions.map do |e|
|
32
34
|
if e.backtrace && !processed_backtrace_ids.include?(e.backtrace.object_id)
|
33
35
|
processed_backtrace_ids << e.backtrace.object_id
|
34
|
-
SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder)
|
36
|
+
SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder, mechanism: mechanism)
|
35
37
|
else
|
36
|
-
SingleExceptionInterface.new(exception: exception)
|
38
|
+
SingleExceptionInterface.new(exception: exception, mechanism: mechanism)
|
37
39
|
end
|
38
40
|
end
|
39
41
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
class Mechanism < Interface
|
5
|
+
# Generic identifier, mostly the source integration for this exception.
|
6
|
+
# @return [String]
|
7
|
+
attr_accessor :type
|
8
|
+
|
9
|
+
# A manually captured exception has handled set to true,
|
10
|
+
# false if coming from an integration where we intercept an uncaught exception.
|
11
|
+
# Defaults to true here and will be set to false explicitly in integrations.
|
12
|
+
# @return [Boolean]
|
13
|
+
attr_accessor :handled
|
14
|
+
|
15
|
+
def initialize(type: "generic", handled: true)
|
16
|
+
@type = type
|
17
|
+
@handled = handled
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
module Sentry
|
4
4
|
class RequestInterface < Interface
|
5
|
-
REQUEST_ID_HEADERS = %w
|
6
|
-
CONTENT_HEADERS = %w
|
5
|
+
REQUEST_ID_HEADERS = %w[action_dispatch.request_id HTTP_X_REQUEST_ID].freeze
|
6
|
+
CONTENT_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH].freeze
|
7
7
|
IP_HEADERS = [
|
8
8
|
"REMOTE_ADDR",
|
9
9
|
"HTTP_CLIENT_IP",
|
@@ -59,7 +59,7 @@ module Sentry
|
|
59
59
|
self.query_string = request.query_string
|
60
60
|
end
|
61
61
|
|
62
|
-
self.url = request.scheme && request.url.split(
|
62
|
+
self.url = request.scheme && request.url.split("?").first
|
63
63
|
self.method = request.request_method
|
64
64
|
|
65
65
|
self.headers = filter_and_format_headers(env, send_default_pii)
|
@@ -85,14 +85,14 @@ module Sentry
|
|
85
85
|
env.each_with_object({}) do |(key, value), memo|
|
86
86
|
begin
|
87
87
|
key = key.to_s # rack env can contain symbols
|
88
|
-
next memo[
|
88
|
+
next memo["X-Request-Id"] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
|
89
89
|
next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
|
90
90
|
next if is_skippable_header?(key)
|
91
91
|
next if key == "HTTP_AUTHORIZATION" && !send_default_pii
|
92
92
|
|
93
93
|
# Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
|
94
94
|
key = key.sub(/^HTTP_/, "")
|
95
|
-
key = key.split(
|
95
|
+
key = key.split("_").map(&:capitalize).join("-")
|
96
96
|
|
97
97
|
memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s)
|
98
98
|
rescue StandardError => e
|
@@ -108,7 +108,7 @@ module Sentry
|
|
108
108
|
def is_skippable_header?(key)
|
109
109
|
key.upcase != key || # lower-case envs aren't real http headers
|
110
110
|
key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
|
111
|
-
!(key.start_with?(
|
111
|
+
!(key.start_with?("HTTP_") || CONTENT_HEADERS.include?(key))
|
112
112
|
end
|
113
113
|
|
114
114
|
# In versions < 3, Rack adds in an incorrect HTTP_VERSION key, which causes downstream
|
@@ -120,7 +120,7 @@ module Sentry
|
|
120
120
|
rack_version = Gem::Version.new(::Rack.release)
|
121
121
|
return false if rack_version >= Gem::Version.new("3.0")
|
122
122
|
|
123
|
-
key ==
|
123
|
+
key == "HTTP_VERSION" && value == protocol_version
|
124
124
|
end
|
125
125
|
|
126
126
|
def filter_and_format_env(env, rack_env_whitelist)
|
@@ -7,14 +7,14 @@ module Sentry
|
|
7
7
|
include CustomInspection
|
8
8
|
|
9
9
|
SKIP_INSPECTION_ATTRIBUTES = [:@stacktrace]
|
10
|
-
PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]"
|
11
|
-
OMISSION_MARK = "..."
|
10
|
+
PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]"
|
11
|
+
OMISSION_MARK = "..."
|
12
12
|
MAX_LOCAL_BYTES = 1024
|
13
13
|
|
14
|
-
attr_reader :type, :module, :thread_id, :stacktrace
|
14
|
+
attr_reader :type, :module, :thread_id, :stacktrace, :mechanism
|
15
15
|
attr_accessor :value
|
16
16
|
|
17
|
-
def initialize(exception:, stacktrace: nil)
|
17
|
+
def initialize(exception:, mechanism:, stacktrace: nil)
|
18
18
|
@type = exception.class.to_s
|
19
19
|
exception_message =
|
20
20
|
if exception.respond_to?(:detailed_message)
|
@@ -26,20 +26,22 @@ module Sentry
|
|
26
26
|
|
27
27
|
@value = Utils::EncodingHelper.encode_to_utf_8(exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES))
|
28
28
|
|
29
|
-
@module = exception.class.to_s.split(
|
29
|
+
@module = exception.class.to_s.split("::")[0...-1].join("::")
|
30
30
|
@thread_id = Thread.current.object_id
|
31
31
|
@stacktrace = stacktrace
|
32
|
+
@mechanism = mechanism
|
32
33
|
end
|
33
34
|
|
34
35
|
def to_hash
|
35
36
|
data = super
|
36
37
|
data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
|
38
|
+
data[:mechanism] = data[:mechanism].to_hash
|
37
39
|
data
|
38
40
|
end
|
39
41
|
|
40
42
|
# patch this method if you want to change an exception's stacktrace frames
|
41
43
|
# also see `StacktraceBuilder.build`.
|
42
|
-
def self.build_with_stacktrace(exception:, stacktrace_builder:)
|
44
|
+
def self.build_with_stacktrace(exception:, stacktrace_builder:, mechanism:)
|
43
45
|
stacktrace = stacktrace_builder.build(backtrace: exception.backtrace)
|
44
46
|
|
45
47
|
if locals = exception.instance_variable_get(:@sentry_locals)
|
@@ -61,7 +63,7 @@ module Sentry
|
|
61
63
|
stacktrace.frames.last.vars = locals
|
62
64
|
end
|
63
65
|
|
64
|
-
new(exception: exception, stacktrace: stacktrace)
|
66
|
+
new(exception: exception, stacktrace: stacktrace, mechanism: mechanism)
|
65
67
|
end
|
66
68
|
end
|
67
69
|
end
|
@@ -27,8 +27,9 @@ module Sentry
|
|
27
27
|
attr_accessor :abs_path, :context_line, :function, :in_app, :filename,
|
28
28
|
:lineno, :module, :pre_context, :post_context, :vars
|
29
29
|
|
30
|
-
def initialize(project_root, line)
|
30
|
+
def initialize(project_root, line, strip_backtrace_load_path = true)
|
31
31
|
@project_root = project_root
|
32
|
+
@strip_backtrace_load_path = strip_backtrace_load_path
|
32
33
|
|
33
34
|
@abs_path = line.file
|
34
35
|
@function = line.method if line.method
|
@@ -44,6 +45,7 @@ module Sentry
|
|
44
45
|
|
45
46
|
def compute_filename
|
46
47
|
return if abs_path.nil?
|
48
|
+
return abs_path unless @strip_backtrace_load_path
|
47
49
|
|
48
50
|
prefix =
|
49
51
|
if under_project_root? && in_app
|
@@ -17,22 +17,35 @@ module Sentry
|
|
17
17
|
# @return [Proc, nil]
|
18
18
|
attr_reader :backtrace_cleanup_callback
|
19
19
|
|
20
|
+
# @return [Boolean]
|
21
|
+
attr_reader :strip_backtrace_load_path
|
22
|
+
|
20
23
|
# @param project_root [String]
|
21
24
|
# @param app_dirs_pattern [Regexp, nil]
|
22
25
|
# @param linecache [LineCache]
|
23
26
|
# @param context_lines [Integer, nil]
|
24
27
|
# @param backtrace_cleanup_callback [Proc, nil]
|
28
|
+
# @param strip_backtrace_load_path [Boolean]
|
25
29
|
# @see Configuration#project_root
|
26
30
|
# @see Configuration#app_dirs_pattern
|
27
31
|
# @see Configuration#linecache
|
28
32
|
# @see Configuration#context_lines
|
29
33
|
# @see Configuration#backtrace_cleanup_callback
|
30
|
-
|
34
|
+
# @see Configuration#strip_backtrace_load_path
|
35
|
+
def initialize(
|
36
|
+
project_root:,
|
37
|
+
app_dirs_pattern:,
|
38
|
+
linecache:,
|
39
|
+
context_lines:,
|
40
|
+
backtrace_cleanup_callback: nil,
|
41
|
+
strip_backtrace_load_path: true
|
42
|
+
)
|
31
43
|
@project_root = project_root
|
32
44
|
@app_dirs_pattern = app_dirs_pattern
|
33
45
|
@linecache = linecache
|
34
46
|
@context_lines = context_lines
|
35
47
|
@backtrace_cleanup_callback = backtrace_cleanup_callback
|
48
|
+
@strip_backtrace_load_path = strip_backtrace_load_path
|
36
49
|
end
|
37
50
|
|
38
51
|
# Generates a StacktraceInterface with the given backtrace.
|
@@ -62,10 +75,18 @@ module Sentry
|
|
62
75
|
StacktraceInterface.new(frames: frames)
|
63
76
|
end
|
64
77
|
|
78
|
+
# Get the code location hash for a single line for where metrics where added.
|
79
|
+
# @return [Hash]
|
80
|
+
def metrics_code_location(unparsed_line)
|
81
|
+
parsed_line = Backtrace::Line.parse(unparsed_line)
|
82
|
+
frame = convert_parsed_line_into_frame(parsed_line)
|
83
|
+
frame.to_hash.reject { |k, _| %i[project_root in_app].include?(k) }
|
84
|
+
end
|
85
|
+
|
65
86
|
private
|
66
87
|
|
67
88
|
def convert_parsed_line_into_frame(line)
|
68
|
-
frame = StacktraceInterface::Frame.new(project_root, line)
|
89
|
+
frame = StacktraceInterface::Frame.new(project_root, line, strip_backtrace_load_path)
|
69
90
|
frame.set_context(linecache, context_lines) if context_lines
|
70
91
|
frame
|
71
92
|
end
|
data/lib/sentry/logger.rb
CHANGED
@@ -0,0 +1,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Metrics
|
5
|
+
class Aggregator < ThreadedPeriodicWorker
|
6
|
+
FLUSH_INTERVAL = 5
|
7
|
+
ROLLUP_IN_SECONDS = 10
|
8
|
+
|
9
|
+
# this is how far removed from user code in the backtrace we are
|
10
|
+
# when we record code locations
|
11
|
+
DEFAULT_STACKLEVEL = 4
|
12
|
+
|
13
|
+
KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\-.]+/
|
14
|
+
UNIT_SANITIZATION_REGEX = /[^a-zA-Z0-9_]+/
|
15
|
+
TAG_KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\-.\/]+/
|
16
|
+
|
17
|
+
TAG_VALUE_SANITIZATION_MAP = {
|
18
|
+
"\n" => "\\n",
|
19
|
+
"\r" => "\\r",
|
20
|
+
"\t" => "\\t",
|
21
|
+
"\\" => "\\\\",
|
22
|
+
"|" => "\\u{7c}",
|
23
|
+
"," => "\\u{2c}"
|
24
|
+
}
|
25
|
+
|
26
|
+
METRIC_TYPES = {
|
27
|
+
c: CounterMetric,
|
28
|
+
d: DistributionMetric,
|
29
|
+
g: GaugeMetric,
|
30
|
+
s: SetMetric
|
31
|
+
}
|
32
|
+
|
33
|
+
# exposed only for testing
|
34
|
+
attr_reader :client, :thread, :buckets, :flush_shift, :code_locations
|
35
|
+
|
36
|
+
def initialize(configuration, client)
|
37
|
+
super(configuration.logger, FLUSH_INTERVAL)
|
38
|
+
@client = client
|
39
|
+
@before_emit = configuration.metrics.before_emit
|
40
|
+
@enable_code_locations = configuration.metrics.enable_code_locations
|
41
|
+
@stacktrace_builder = configuration.stacktrace_builder
|
42
|
+
|
43
|
+
@default_tags = {}
|
44
|
+
@default_tags["release"] = configuration.release if configuration.release
|
45
|
+
@default_tags["environment"] = configuration.environment if configuration.environment
|
46
|
+
|
47
|
+
@mutex = Mutex.new
|
48
|
+
|
49
|
+
# a nested hash of timestamp -> bucket keys -> Metric instance
|
50
|
+
@buckets = {}
|
51
|
+
|
52
|
+
# the flush interval needs to be shifted once per startup to create jittering
|
53
|
+
@flush_shift = Random.rand * ROLLUP_IN_SECONDS
|
54
|
+
|
55
|
+
# a nested hash of timestamp (start of day) -> meta keys -> frame
|
56
|
+
@code_locations = {}
|
57
|
+
end
|
58
|
+
|
59
|
+
def add(type,
|
60
|
+
key,
|
61
|
+
value,
|
62
|
+
unit: "none",
|
63
|
+
tags: {},
|
64
|
+
timestamp: nil,
|
65
|
+
stacklevel: nil)
|
66
|
+
return unless ensure_thread
|
67
|
+
return unless METRIC_TYPES.keys.include?(type)
|
68
|
+
|
69
|
+
updated_tags = get_updated_tags(tags)
|
70
|
+
return if @before_emit && !@before_emit.call(key, updated_tags)
|
71
|
+
|
72
|
+
timestamp ||= Sentry.utc_now
|
73
|
+
|
74
|
+
# this is integer division and thus takes the floor of the division
|
75
|
+
# and buckets into 10 second intervals
|
76
|
+
bucket_timestamp = (timestamp.to_i / ROLLUP_IN_SECONDS) * ROLLUP_IN_SECONDS
|
77
|
+
|
78
|
+
serialized_tags = serialize_tags(updated_tags)
|
79
|
+
bucket_key = [type, key, unit, serialized_tags]
|
80
|
+
|
81
|
+
added = @mutex.synchronize do
|
82
|
+
record_code_location(type, key, unit, timestamp, stacklevel: stacklevel) if @enable_code_locations
|
83
|
+
process_bucket(bucket_timestamp, bucket_key, type, value)
|
84
|
+
end
|
85
|
+
|
86
|
+
# for sets, we pass on if there was a new entry to the local gauge
|
87
|
+
local_value = type == :s ? added : value
|
88
|
+
process_span_aggregator(bucket_key, local_value)
|
89
|
+
end
|
90
|
+
|
91
|
+
def flush(force: false)
|
92
|
+
flushable_buckets = get_flushable_buckets!(force)
|
93
|
+
code_locations = get_code_locations!
|
94
|
+
return if flushable_buckets.empty? && code_locations.empty?
|
95
|
+
|
96
|
+
envelope = Envelope.new
|
97
|
+
|
98
|
+
unless flushable_buckets.empty?
|
99
|
+
payload = serialize_buckets(flushable_buckets)
|
100
|
+
envelope.add_item(
|
101
|
+
{ type: "statsd", length: payload.bytesize },
|
102
|
+
payload
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
unless code_locations.empty?
|
107
|
+
code_locations.each do |timestamp, locations|
|
108
|
+
payload = serialize_locations(timestamp, locations)
|
109
|
+
envelope.add_item(
|
110
|
+
{ type: "metric_meta", content_type: "application/json" },
|
111
|
+
payload
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
@client.capture_envelope(envelope)
|
117
|
+
end
|
118
|
+
|
119
|
+
alias_method :run, :flush
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# important to sort for key consistency
|
124
|
+
def serialize_tags(tags)
|
125
|
+
tags.flat_map do |k, v|
|
126
|
+
if v.is_a?(Array)
|
127
|
+
v.map { |x| [k.to_s, x.to_s] }
|
128
|
+
else
|
129
|
+
[[k.to_s, v.to_s]]
|
130
|
+
end
|
131
|
+
end.sort
|
132
|
+
end
|
133
|
+
|
134
|
+
def get_flushable_buckets!(force)
|
135
|
+
@mutex.synchronize do
|
136
|
+
flushable_buckets = {}
|
137
|
+
|
138
|
+
if force
|
139
|
+
flushable_buckets = @buckets
|
140
|
+
@buckets = {}
|
141
|
+
else
|
142
|
+
cutoff = Sentry.utc_now.to_i - ROLLUP_IN_SECONDS - @flush_shift
|
143
|
+
flushable_buckets = @buckets.select { |k, _| k <= cutoff }
|
144
|
+
@buckets.reject! { |k, _| k <= cutoff }
|
145
|
+
end
|
146
|
+
|
147
|
+
flushable_buckets
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def get_code_locations!
|
152
|
+
@mutex.synchronize do
|
153
|
+
code_locations = @code_locations
|
154
|
+
@code_locations = {}
|
155
|
+
code_locations
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# serialize buckets to statsd format
|
160
|
+
def serialize_buckets(buckets)
|
161
|
+
buckets.map do |timestamp, timestamp_buckets|
|
162
|
+
timestamp_buckets.map do |metric_key, metric|
|
163
|
+
type, key, unit, tags = metric_key
|
164
|
+
values = metric.serialize.join(":")
|
165
|
+
sanitized_tags = tags.map { |k, v| "#{sanitize_tag_key(k)}:#{sanitize_tag_value(v)}" }.join(",")
|
166
|
+
|
167
|
+
"#{sanitize_key(key)}@#{sanitize_unit(unit)}:#{values}|#{type}|\##{sanitized_tags}|T#{timestamp}"
|
168
|
+
end
|
169
|
+
end.flatten.join("\n")
|
170
|
+
end
|
171
|
+
|
172
|
+
def serialize_locations(timestamp, locations)
|
173
|
+
mapping = locations.map do |meta_key, location|
|
174
|
+
type, key, unit = meta_key
|
175
|
+
mri = "#{type}:#{sanitize_key(key)}@#{sanitize_unit(unit)}"
|
176
|
+
|
177
|
+
# note this needs to be an array but it really doesn't serve a purpose right now
|
178
|
+
[mri, [location.merge(type: "location")]]
|
179
|
+
end.to_h
|
180
|
+
|
181
|
+
{ timestamp: timestamp, mapping: mapping }
|
182
|
+
end
|
183
|
+
|
184
|
+
def sanitize_key(key)
|
185
|
+
key.gsub(KEY_SANITIZATION_REGEX, "_")
|
186
|
+
end
|
187
|
+
|
188
|
+
def sanitize_unit(unit)
|
189
|
+
unit.gsub(UNIT_SANITIZATION_REGEX, "")
|
190
|
+
end
|
191
|
+
|
192
|
+
def sanitize_tag_key(key)
|
193
|
+
key.gsub(TAG_KEY_SANITIZATION_REGEX, "")
|
194
|
+
end
|
195
|
+
|
196
|
+
def sanitize_tag_value(value)
|
197
|
+
value.chars.map { |c| TAG_VALUE_SANITIZATION_MAP[c] || c }.join
|
198
|
+
end
|
199
|
+
|
200
|
+
def get_transaction_name
|
201
|
+
scope = Sentry.get_current_scope
|
202
|
+
return nil unless scope && scope.transaction_name
|
203
|
+
return nil if scope.transaction_source_low_quality?
|
204
|
+
|
205
|
+
scope.transaction_name
|
206
|
+
end
|
207
|
+
|
208
|
+
def get_updated_tags(tags)
|
209
|
+
updated_tags = @default_tags.merge(tags)
|
210
|
+
|
211
|
+
transaction_name = get_transaction_name
|
212
|
+
updated_tags["transaction"] = transaction_name if transaction_name
|
213
|
+
|
214
|
+
updated_tags
|
215
|
+
end
|
216
|
+
|
217
|
+
def process_span_aggregator(key, value)
|
218
|
+
scope = Sentry.get_current_scope
|
219
|
+
return nil unless scope && scope.span
|
220
|
+
return nil if scope.transaction_source_low_quality?
|
221
|
+
|
222
|
+
scope.span.metrics_local_aggregator.add(key, value)
|
223
|
+
end
|
224
|
+
|
225
|
+
def process_bucket(timestamp, key, type, value)
|
226
|
+
@buckets[timestamp] ||= {}
|
227
|
+
|
228
|
+
if (metric = @buckets[timestamp][key])
|
229
|
+
old_weight = metric.weight
|
230
|
+
metric.add(value)
|
231
|
+
metric.weight - old_weight
|
232
|
+
else
|
233
|
+
metric = METRIC_TYPES[type].new(value)
|
234
|
+
@buckets[timestamp][key] = metric
|
235
|
+
metric.weight
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def record_code_location(type, key, unit, timestamp, stacklevel: nil)
|
240
|
+
meta_key = [type, key, unit]
|
241
|
+
start_of_day = Time.utc(timestamp.year, timestamp.month, timestamp.day).to_i
|
242
|
+
|
243
|
+
@code_locations[start_of_day] ||= {}
|
244
|
+
@code_locations[start_of_day][meta_key] ||= @stacktrace_builder.metrics_code_location(caller[stacklevel || DEFAULT_STACKLEVEL])
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Metrics
|
5
|
+
class Configuration
|
6
|
+
include ArgumentCheckingHelper
|
7
|
+
|
8
|
+
# Enable metrics usage.
|
9
|
+
# Starts a new {Sentry::Metrics::Aggregator} instance to aggregate metrics
|
10
|
+
# and a thread to aggregate flush every 5 seconds.
|
11
|
+
# @return [Boolean]
|
12
|
+
attr_accessor :enabled
|
13
|
+
|
14
|
+
# Enable code location reporting.
|
15
|
+
# Will be sent once per day.
|
16
|
+
# True by default.
|
17
|
+
# @return [Boolean]
|
18
|
+
attr_accessor :enable_code_locations
|
19
|
+
|
20
|
+
# Optional Proc, called before emitting a metric to the aggregator.
|
21
|
+
# Use it to filter keys (return false/nil) or update tags.
|
22
|
+
# Make sure to return true at the end.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# config.metrics.before_emit = lambda do |key, tags|
|
26
|
+
# return nil if key == 'foo'
|
27
|
+
# tags[:bar] = 42
|
28
|
+
# tags.delete(:baz)
|
29
|
+
# true
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @return [Proc, nil]
|
33
|
+
attr_reader :before_emit
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@enabled = false
|
37
|
+
@enable_code_locations = true
|
38
|
+
end
|
39
|
+
|
40
|
+
def before_emit=(value)
|
41
|
+
check_callable!("metrics.before_emit", value)
|
42
|
+
|
43
|
+
@before_emit = value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Metrics
|
5
|
+
class CounterMetric < Metric
|
6
|
+
attr_reader :value
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
@value = value.to_f
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(value)
|
13
|
+
@value += value.to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
def serialize
|
17
|
+
[value]
|
18
|
+
end
|
19
|
+
|
20
|
+
def weight
|
21
|
+
1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Metrics
|
5
|
+
class DistributionMetric < Metric
|
6
|
+
attr_reader :value
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
@value = [value.to_f]
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(value)
|
13
|
+
@value << value.to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
def serialize
|
17
|
+
value
|
18
|
+
end
|
19
|
+
|
20
|
+
def weight
|
21
|
+
value.size
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|