sentry-ruby 5.16.1 → 5.19.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 +3 -0
- data/README.md +20 -10
- data/Rakefile +1 -1
- data/bin/console +1 -0
- data/lib/sentry/attachment.rb +42 -0
- data/lib/sentry/background_worker.rb +1 -1
- data/lib/sentry/backpressure_monitor.rb +2 -32
- data/lib/sentry/backtrace.rb +7 -3
- data/lib/sentry/check_in_event.rb +1 -1
- data/lib/sentry/client.rb +59 -9
- data/lib/sentry/configuration.rb +25 -12
- data/lib/sentry/cron/monitor_schedule.rb +1 -1
- data/lib/sentry/dsn.rb +1 -1
- data/lib/sentry/envelope.rb +18 -1
- data/lib/sentry/error_event.rb +2 -2
- data/lib/sentry/event.rb +13 -11
- data/lib/sentry/faraday.rb +77 -0
- data/lib/sentry/graphql.rb +9 -0
- data/lib/sentry/hub.rb +15 -2
- 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 +2 -2
- data/lib/sentry/interfaces/single_exception.rb +6 -4
- data/lib/sentry/interfaces/stacktrace_builder.rb +8 -0
- 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 +17 -38
- data/lib/sentry/propagation_context.rb +9 -8
- data/lib/sentry/puma.rb +1 -1
- data/lib/sentry/rack/capture_exceptions.rb +14 -2
- data/lib/sentry/rake.rb +3 -1
- data/lib/sentry/redis.rb +2 -1
- data/lib/sentry/scope.rb +35 -26
- data/lib/sentry/session.rb +2 -2
- data/lib/sentry/session_flusher.rb +6 -38
- data/lib/sentry/span.rb +40 -5
- data/lib/sentry/test_helper.rb +2 -1
- data/lib/sentry/threaded_periodic_worker.rb +39 -0
- data/lib/sentry/transaction.rb +16 -14
- data/lib/sentry/transaction_event.rb +5 -0
- data/lib/sentry/transport/configuration.rb +0 -1
- data/lib/sentry/transport.rb +14 -22
- data/lib/sentry/utils/argument_checking_helper.rb +6 -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 +1 -1
- data/lib/sentry/utils/request_id.rb +1 -1
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +34 -3
- data/sentry-ruby.gemspec +12 -5
- metadata +39 -7
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Faraday
|
5
|
+
OP_NAME = "http.client"
|
6
|
+
|
7
|
+
module Connection
|
8
|
+
# Since there's no way to preconfigure Faraday connections and add our instrumentation
|
9
|
+
# by default, we need to extend the connection constructor and do it there
|
10
|
+
#
|
11
|
+
# @see https://lostisland.github.io/faraday/#/customization/index?id=configuration
|
12
|
+
def initialize(url = nil, options = nil)
|
13
|
+
super
|
14
|
+
|
15
|
+
# Ensure that we attach instrumentation only if the adapter is not net/http
|
16
|
+
# because if is is, then the net/http instrumentation will take care of it
|
17
|
+
if builder.adapter.name != "Faraday::Adapter::NetHttp"
|
18
|
+
# Make sure that it's going to be the first middleware so that it can capture
|
19
|
+
# the entire request processing involving other middlewares
|
20
|
+
builder.insert(0, ::Faraday::Request::Instrumentation, name: OP_NAME, instrumenter: Instrumenter.new)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Instrumenter
|
26
|
+
SPAN_ORIGIN = "auto.http.faraday"
|
27
|
+
BREADCRUMB_CATEGORY = "http"
|
28
|
+
|
29
|
+
include Utils::HttpTracing
|
30
|
+
|
31
|
+
def instrument(op_name, env, &block)
|
32
|
+
return block.call unless Sentry.initialized?
|
33
|
+
|
34
|
+
Sentry.with_child_span(op: op_name, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |sentry_span|
|
35
|
+
request_info = extract_request_info(env)
|
36
|
+
|
37
|
+
if propagate_trace?(request_info[:url])
|
38
|
+
set_propagation_headers(env[:request_headers])
|
39
|
+
end
|
40
|
+
|
41
|
+
res = block.call
|
42
|
+
response_status = res.status
|
43
|
+
|
44
|
+
if record_sentry_breadcrumb?
|
45
|
+
record_sentry_breadcrumb(request_info, response_status)
|
46
|
+
end
|
47
|
+
|
48
|
+
if sentry_span
|
49
|
+
set_span_info(sentry_span, request_info, response_status)
|
50
|
+
end
|
51
|
+
|
52
|
+
res
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def extract_request_info(env)
|
59
|
+
url = env[:url].scheme + "://" + env[:url].host + env[:url].path
|
60
|
+
result = { method: env[:method].to_s.upcase, url: url }
|
61
|
+
|
62
|
+
if Sentry.configuration.send_default_pii
|
63
|
+
result[:query] = env[:url].query
|
64
|
+
result[:body] = env[:body]
|
65
|
+
end
|
66
|
+
|
67
|
+
result
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
Sentry.register_patch(:faraday) do
|
74
|
+
if defined?(::Faraday)
|
75
|
+
::Faraday::Connection.prepend(Sentry::Faraday::Connection)
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Sentry.register_patch(:graphql) do |config|
|
4
|
+
if defined?(::GraphQL::Schema) && defined?(::GraphQL::Tracing::SentryTrace) && ::GraphQL::Schema.respond_to?(:trace_with)
|
5
|
+
::GraphQL::Schema.trace_with(::GraphQL::Tracing::SentryTrace, set_transaction_name: true)
|
6
|
+
else
|
7
|
+
config.logger.warn(Sentry::LOGGER_PROGNAME) { 'You tried to enable the GraphQL integration but no GraphQL gem was detected. Make sure you have the `graphql` gem (>= 2.2.6) in your Gemfile.' }
|
8
|
+
end
|
9
|
+
end
|
data/lib/sentry/hub.rb
CHANGED
@@ -193,7 +193,14 @@ module Sentry
|
|
193
193
|
elsif custom_scope = options[:scope]
|
194
194
|
scope.update_from_scope(custom_scope)
|
195
195
|
elsif !options.empty?
|
196
|
-
scope.update_from_options(**options)
|
196
|
+
unsupported_option_keys = scope.update_from_options(**options)
|
197
|
+
|
198
|
+
unless unsupported_option_keys.empty?
|
199
|
+
configuration.log_debug <<~MSG
|
200
|
+
Options #{unsupported_option_keys} are not supported and will not be applied to the event.
|
201
|
+
You may want to set them under the `extra` option.
|
202
|
+
MSG
|
203
|
+
end
|
197
204
|
end
|
198
205
|
|
199
206
|
event = current_client.capture_event(event, scope, hint)
|
@@ -245,7 +252,7 @@ module Sentry
|
|
245
252
|
end
|
246
253
|
|
247
254
|
def with_session_tracking(&block)
|
248
|
-
return yield unless configuration.
|
255
|
+
return yield unless configuration.session_tracking?
|
249
256
|
|
250
257
|
start_session
|
251
258
|
yield
|
@@ -279,6 +286,12 @@ module Sentry
|
|
279
286
|
headers
|
280
287
|
end
|
281
288
|
|
289
|
+
def get_trace_propagation_meta
|
290
|
+
get_trace_propagation_headers.map do |k, v|
|
291
|
+
"<meta name=\"#{k}\" content=\"#{v}\">"
|
292
|
+
end.join("\n")
|
293
|
+
end
|
294
|
+
|
282
295
|
def continue_trace(env, **options)
|
283
296
|
configure_scope { |s| s.generate_propagation_context(env) }
|
284
297
|
|
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",
|
@@ -11,10 +11,10 @@ module Sentry
|
|
11
11
|
OMISSION_MARK = "...".freeze
|
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)
|
@@ -29,17 +29,19 @@ module Sentry
|
|
29
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
|
@@ -62,6 +62,14 @@ module Sentry
|
|
62
62
|
StacktraceInterface.new(frames: frames)
|
63
63
|
end
|
64
64
|
|
65
|
+
# Get the code location hash for a single line for where metrics where added.
|
66
|
+
# @return [Hash]
|
67
|
+
def metrics_code_location(unparsed_line)
|
68
|
+
parsed_line = Backtrace::Line.parse(unparsed_line)
|
69
|
+
frame = convert_parsed_line_into_frame(parsed_line)
|
70
|
+
frame.to_hash.reject { |k, _| %i[project_root in_app].include?(k) }
|
71
|
+
end
|
72
|
+
|
65
73
|
private
|
66
74
|
|
67
75
|
def convert_parsed_line_into_frame(line)
|
@@ -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
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
module Metrics
|
5
|
+
class GaugeMetric < Metric
|
6
|
+
attr_reader :last, :min, :max, :sum, :count
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
value = value.to_f
|
10
|
+
@last = value
|
11
|
+
@min = value
|
12
|
+
@max = value
|
13
|
+
@sum = value
|
14
|
+
@count = 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(value)
|
18
|
+
value = value.to_f
|
19
|
+
@last = value
|
20
|
+
@min = [@min, value].min
|
21
|
+
@max = [@max, value].max
|
22
|
+
@sum += value
|
23
|
+
@count += 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def serialize
|
27
|
+
[last, min, max, sum, count]
|
28
|
+
end
|
29
|
+
|
30
|
+
def weight
|
31
|
+
5
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|