sentry-ruby 5.16.1 → 5.21.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|